Compare commits

...

65 Commits

Author SHA1 Message Date
Ferdinand Schober
09f08f1798 fix drop impl for desktop-portal 2024-07-05 00:31:16 +02:00
Ferdinand Schober
f97621e987 adjust error handling 2024-07-04 23:44:32 +02:00
Ferdinand Schober
b3aa3f4281 remove dispatch workaround 2024-07-04 22:40:05 +02:00
Ferdinand Schober
9abec63313 update dependencies 2024-07-03 11:11:50 +02:00
Ferdinand Schober
35e626976e add explicit version 2024-07-02 22:39:20 +02:00
Ferdinand Schober
684735b499 fix dependencies in input-event 2024-07-02 22:34:43 +02:00
Ferdinand Schober
abfc744e4c specify dependency versions explicitly 2024-07-02 22:17:26 +02:00
Ferdinand Schober
fb2c39e8ae fix all features enabled by default 2024-07-02 22:07:37 +02:00
Ferdinand Schober
82ab5ecbbd fix xdg-desktop-portal backend not available 2024-07-02 22:07:37 +02:00
Ferdinand Schober
3fd2b31562 update deps 2024-07-02 22:07:37 +02:00
Ferdinand Schober
90e83cee87 purge dependencies 2024-07-02 22:07:37 +02:00
Ferdinand Schober
4db2d37f32 split into input-{event,capture,emulation} 2024-07-02 22:07:37 +02:00
Ferdinand Schober
7b511bb97d fix iteration order 2024-07-02 16:23:07 +02:00
Ferdinand Schober
70a23b9fa7 reduce coupling of emulation and capture backends 2024-07-02 16:23:07 +02:00
Ferdinand Schober
b6b16063a8 Configurable emulation backend (#151) 2024-07-01 20:09:16 +02:00
Ferdinand Schober
9cbe1ed8d8 fix release bind message 2024-06-30 14:41:16 +02:00
Ferdinand Schober
3528ef4fae Configurable capture backend (#150)
capture backend can now be configured via the `capture_backend` cli argument / config entry
2024-06-29 00:10:36 +02:00
Ferdinand Schober
232c048c19 fix transmuting to pointer types UB (#147)
closes #134
2024-06-25 14:13:05 +02:00
Orhun Parmaksız
1c37579ae5 Update Arch Linux instructions (#145) 2024-06-19 20:28:30 +02:00
Ferdinand Schober
460bacade5 fix sizeof usize assumed to be 8 (#143)
closes #141
2024-06-09 00:49:00 +02:00
虢豳
5fd3b719d6 Extract package name and version from Cargo.toml (#136)
* chore: nix flake update

* feat: Extract package name and version from Cargo.toml
2024-05-22 08:06:44 +02:00
Ferdinand Schober
e6d4585bb2 chore: Release lan-mouse version 0.8.0 2024-05-17 18:06:11 +02:00
Ferdinand Schober
1c082d5c0c fix win -> lin scancode translations for intl keys 2024-05-17 17:39:52 +02:00
Ferdinand Schober
cd98acbd08 fix right shift not working 2024-05-16 22:36:51 +02:00
Ferdinand Schober
11e1919588 fix most international keybindings 2024-05-16 22:29:41 +02:00
Ferdinand Schober
152bceaa86 fix dns resolving 2024-05-13 08:12:55 +02:00
Ferdinand Schober
60180d841c fix formatting 2024-05-12 15:50:03 +02:00
Ferdinand Schober
5802a0be0b fix discrete scrolling on wlroots 2024-05-12 15:49:39 +02:00
Ferdinand Schober
1737727d61 fix cast 2024-05-12 15:07:29 +02:00
Ferdinand Schober
ba46037a1f fix scrolling in windows 2024-05-12 14:55:56 +02:00
Ferdinand Schober
da768b6fb8 ignore Axis events corresponding to v120 events 2024-05-12 13:32:14 +02:00
Ferdinand Schober
799b45104a enter hook command (#130)
new configuration option `enter_hook` can now be used to spawn a command when a client is entered
2024-05-12 13:01:07 +02:00
Ferdinand Schober
e9738fc024 explicit state synchronisation (#129)
prevents unnecessary state updates and makes synchronization less error prone
2024-05-12 00:49:53 +02:00
Ferdinand Schober
9969f997d3 macos-latest on longer runs on intel macs 2024-05-07 11:46:01 +02:00
Ferdinand Schober
b8cc9e2197 hotfix: race condition when activating clients 2024-05-07 11:18:15 +02:00
Ferdinand Schober
ba6abafe75 windows: fix resolution with scaling enabled (#124) 2024-05-07 00:07:30 +02:00
Ferdinand Schober
effb9ce0fa libei: fix touchpad scrolling (#121) 2024-05-04 12:53:45 +02:00
Ferdinand Schober
1f0d386d4a add discrete120 scroll event (#120)
* add discrete120 scroll event
2024-05-04 03:34:13 +02:00
Ferdinand Schober
973360a774 libei emulation: use discrete scroll events 2024-05-04 01:27:38 +02:00
Ferdinand Schober
e21ab02a6e layer-shell: use value120 scroll events #115 2024-05-04 01:27:13 +02:00
Ferdinand Schober
18a3c10f8e Update FUNDING.yml 2024-05-03 16:40:59 +02:00
Ferdinand Schober
c76d9ef7af implement dns indicator (#119) 2024-05-03 13:00:00 +02:00
Ferdinand Schober
5318f5a02d Separate config state (#118)
* change internal api
* frontend now keeps and more correctly reflects backend state
2024-05-03 11:27:06 +02:00
Johan
1e4312b3ce nix: Add macOS launchd service to hm-module module (#110)
Home manager has the possibility to run macOS launchd services. These
are automatically disabled when running on linux.
2024-04-28 12:25:35 +02:00
Ferdinand Schober
77aa96e09a defer creation of input capture / emulation (#117) 2024-04-26 23:59:00 +02:00
Ferdinand Schober
636c5924bf trust_dns_resolver is now hickory_resolver (#116) 2024-04-26 22:52:00 +02:00
Ferdinand Schober
3e96b42067 use slab instead of reinventing the wheel (#112) 2024-04-26 00:10:04 +02:00
Ferdinand Schober
279e582698 Rename FrontendEvent to FrontendRequest (#111)
* rename frontend event and notify
* simplify event names
2024-04-25 22:18:43 +02:00
Johan
9edd2f7f3b nix: enable creating config file via home-manager (#109) 2024-04-25 14:06:31 +02:00
Johan
43c16a537b nix: Add aarch64-darwin package and devshell (#108)
Enable building and developing using nix on aarch64-darwin
2024-04-24 21:27:24 +02:00
Ferdinand Schober
36855a1a17 pub glib-build-tools behind gtk feature flag 2024-04-15 10:14:48 +02:00
Ferdinand Schober
e537cdbc7e properly reset copy icon 2024-04-13 00:28:53 +02:00
Ferdinand Schober
5b76c3bcda add hostname entry row with clipboard button 2024-04-13 00:24:46 +02:00
Ferdinand Schober
81f65dcd3d gtk: use predifined css classes instead of custom ones 2024-04-13 00:24:46 +02:00
Ferdinand Schober
f0099ee535 windows: revert scrolling multiplier 2024-04-12 15:28:21 +02:00
Ferdinand Schober
633d2c346e windows: impl back and forward mouse buttons 2024-04-12 15:28:05 +02:00
Ferdinand Schober
ccb201ea53 formatting 2024-04-11 13:54:11 +02:00
Ferdinand Schober
f7edfecba9 add tests for capture and emulation 2024-04-11 13:53:49 +02:00
Ferdinand Schober
141ea2809d fix a race condition in windows capture 2024-04-11 13:09:38 +02:00
Ferdinand Schober
058097c618 Update OS support table 2024-04-11 04:05:15 +02:00
Ferdinand Schober
f9eeb254d3 Windows Input Capture (#100)
initial support for windows input capture.
Some things need fixing;
- scrolling
- mouse buttons > 2
2024-04-11 03:55:42 +02:00
Ferdinand Schober
9ca7e2378c enforce only one client at a position (#102) 2024-04-10 14:16:19 +02:00
Ferdinand Schober
cc7984c066 fix clippy lint 2024-04-08 21:14:07 +02:00
Ferdinand Schober
1a2645cfbc fix formatting (#101) 2024-04-08 17:52:16 +02:00
Ferdinand Schober
e52febf457 simplify windows installation instructions 2024-04-08 17:48:22 +02:00
74 changed files with 4774 additions and 2502 deletions

1
.github/FUNDING.yml vendored
View File

@@ -1 +1,2 @@
github: [feschber] github: [feschber]
ko_fi: feschber

View File

@@ -3,8 +3,14 @@ name: Binary Cache
on: [push, pull_request, workflow_dispatch] on: [push, pull_request, workflow_dispatch]
jobs: jobs:
nix: nix:
strategy:
matrix:
os:
- ubuntu-latest
- macos-13
- macos-14
name: "Build" name: "Build"
runs-on: ubuntu-latest runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -20,5 +26,15 @@ jobs:
name: lan-mouse name: lan-mouse
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Build lan-mouse - name: Build lan-mouse (x86_64-linux)
run: nix build --print-build-logs if: matrix.os == 'ubuntu-latest'
run: nix build --print-build-logs --show-trace .#packages.x86_64-linux.lan-mouse
- name: Build lan-mouse (x86_64-darwin)
if: matrix.os == 'macos-13'
run: nix build --print-build-logs --show-trace .#packages.x86_64-darwin.lan-mouse
- name: Build lan-mouse (aarch64-darwin)
if: matrix.os == 'macos-14'
run: nix build --print-build-logs --show-trace .#packages.aarch64-darwin.lan-mouse

View File

@@ -78,7 +78,7 @@ jobs:
path: lan-mouse-windows.zip path: lan-mouse-windows.zip
macos-release-build: macos-release-build:
runs-on: macos-latest runs-on: macos-13
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: install dependencies - name: install dependencies

View File

@@ -92,7 +92,7 @@ jobs:
target/debug/*.dll target/debug/*.dll
build-macos: build-macos:
runs-on: macos-latest runs-on: macos-13
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: install dependencies - name: install dependencies

View File

@@ -74,7 +74,7 @@ jobs:
path: lan-mouse-windows.zip path: lan-mouse-windows.zip
macos-release-build: macos-release-build:
runs-on: macos-latest runs-on: macos-13
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: install dependencies - name: install dependencies

1022
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -39,13 +39,13 @@ For an alternative (with slightly different goals) you may check out [Input Leap
The following table shows support for input emulation (to emulate events received from other clients) and The following table shows support for input emulation (to emulate events received from other clients) and
input capture (to send events *to* other clients) on different operating systems: input capture (to send events *to* other clients) on different operating systems:
| Backend | input emulation | input capture | | OS / Desktop Environment | input emulation | input capture |
|---------------------------|--------------------------|--------------------------------------| |---------------------------|--------------------------|--------------------------------------|
| Wayland (wlroots) | :heavy_check_mark: | :heavy_check_mark: | | Wayland (wlroots) | :heavy_check_mark: | :heavy_check_mark: |
| Wayland (KDE) | :heavy_check_mark: | :heavy_check_mark: | | Wayland (KDE) | :heavy_check_mark: | :heavy_check_mark: |
| Wayland (Gnome) | :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 | | X11 | :heavy_check_mark: | WIP |
| Windows | :heavy_check_mark: | WIP |
| MacOS | :heavy_check_mark: | WIP | | MacOS | :heavy_check_mark: | WIP |
> [!Important] > [!Important]
@@ -58,18 +58,26 @@ input capture (to send events *to* other clients) on different operating systems
> Otherwise input capture will not work. > Otherwise input capture will not work.
## Installation ## Installation
### Install with cargo ### Install via cargo
```sh ```sh
cargo install lan-mouse cargo install lan-mouse
``` ```
### Download from Releases ### Download from Releases
The easiest way to install Lan Mouse is to download precompiled release binaries from the [releases section](https://github.com/feschber/lan-mouse/releases). Precompiled release binaries for Windows, MacOS and Linux are available in the [releases section](https://github.com/feschber/lan-mouse/releases).
For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies). For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies).
### Arch Linux ### Arch Linux
Lan Mouse is available on the AUR:
Lan Mouse can be installed from the [official repositories](https://archlinux.org/packages/extra/x86_64/lan-mouse/):
```sh
pacman -S lan-mouse
```
It is also available on the AUR:
```sh ```sh
# git version (includes latest changes) # git version (includes latest changes)
paru -S lan-mouse-git paru -S lan-mouse-git
@@ -83,7 +91,9 @@ paru -S lan-mouse-bin
- flake: [README.md](./nix/README.md) - flake: [README.md](./nix/README.md)
### Building from Source ### Manual Installation
First make sure to [install the necessary dependencies](#installing-dependencies).
Build in release mode: Build in release mode:
```sh ```sh
@@ -138,56 +148,61 @@ For a detailed list of available features, checkout the [Cargo.toml](./Cargo.tom
## Installing Dependencies ## Installing Dependencies
<details>
<summary>MacOS</summary>
#### Macos
```sh ```sh
brew install libadwaita brew install libadwaita
``` ```
</details>
<details>
<summary>Ubuntu and derivatives</summary>
#### Ubuntu and derivatives
```sh ```sh
sudo apt install libadwaita-1-dev libgtk-4-dev libx11-dev libxtst-dev sudo apt install libadwaita-1-dev libgtk-4-dev libx11-dev libxtst-dev
``` ```
</details>
<details>
<summary>Arch and derivatives</summary>
#### Arch and derivatives
```sh ```sh
sudo pacman -S libadwaita gtk libx11 libxtst sudo pacman -S libadwaita gtk libx11 libxtst
``` ```
</details>
<details>
<summary>Fedora and derivatives</summary>
#### Fedora and derivatives
```sh ```sh
sudo dnf install libadwaita-devel libXtst-devel libX11-devel sudo dnf install libadwaita-devel libXtst-devel libX11-devel
``` ```
</details>
<details>
<summary>Windows</summary>
#### Windows
> [!NOTE] > [!NOTE]
> This is only necessary when building lan-mouse from source. The windows release comes with precompiled gtk dlls. > This is only necessary when building lan-mouse from source. The windows release comes with precompiled gtk dlls.
Follow the instructions at [gtk-rs.org](https://gtk-rs.org/gtk4-rs/stable/latest/book/installation_windows.html) - First install [Rust](https://www.rust-lang.org/tools/install).
- Then follow the instructions at [gtk-rs.org](https://gtk-rs.org/gtk4-rs/stable/latest/book/installation_windows.html)
*TLDR:* *TLDR:*
Build gtk from source Build gtk from source
- The following commands should be run in an admin power shell instance: - The following commands should be run in an **admin power shell** instance:
```sh ```sh
# install chocolatey # install chocolatey
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
# install python 3.11 (Version is important, as 3.12 does not work currently) -> Has been fixed recently # install gvsbuild dependencies
choco install python --version=3.11.0 choco install python git msys2 visualstudio2022-workload-vctools
# install git
choco install git
# install msys2
choco install msys2
# install Visual Studio 2022
choco install visualstudio2022-workload-vctools
``` ```
- The following commands should be run in a regular power shell instance: - The following commands should be run in a **regular power shell** instance:
```sh ```sh
# install gvsbuild with python # install gvsbuild with python
@@ -203,11 +218,12 @@ pipx install gvsbuild
gvsbuild build gtk4 libadwaita librsvg gvsbuild build gtk4 libadwaita librsvg
``` ```
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 below).
</details>
## Usage ## Usage
### Gtk Frontend ### Gtk Frontend

View File

@@ -1,5 +1,6 @@
fn main() { fn main() {
// composite_templates // composite_templates
#[cfg(feature = "gtk")]
glib_build_tools::compile_resources( glib_build_tools::compile_resources(
&["resources"], &["resources"],
"resources/resources.gresource.xml", "resources/resources.gresource.xml",

View File

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

12
flake.lock generated
View File

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

View File

@@ -15,6 +15,8 @@
}: let }: let
inherit (nixpkgs) lib; inherit (nixpkgs) lib;
genSystems = lib.genAttrs [ genSystems = lib.genAttrs [
"aarch64-darwin"
"x86_64-darwin"
"x86_64-linux" "x86_64-linux"
]; ];
pkgsFor = system: pkgsFor = system:
@@ -49,6 +51,8 @@
gtk4 gtk4
libadwaita libadwaita
xorg.libXtst xorg.libXtst
] ++ lib.optionals stdenv.isDarwin [
darwin.apple_sdk_11_0.frameworks.CoreGraphics
]; ];
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library"; RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";

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

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

View File

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

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

@@ -0,0 +1,142 @@
use std::fmt::Display;
use thiserror::Error;
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
use std::io;
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
use wayland_client::{
backend::WaylandError,
globals::{BindError, GlobalError},
ConnectError, DispatchError,
};
#[derive(Debug, Error)]
pub enum CaptureCreationError {
NoAvailableBackend,
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Libei(#[from] LibeiCaptureCreationError),
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
LayerShell(#[from] LayerShellCaptureCreationError),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
X11(#[from] X11InputCaptureCreationError),
#[cfg(target_os = "macos")]
Macos(#[from] MacOSInputCaptureCreationError),
#[cfg(windows)]
Windows,
}
impl Display for CaptureCreationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let reason = match self {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
CaptureCreationError::Libei(reason) => {
format!("error creating portal backend: {reason}")
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
CaptureCreationError::LayerShell(reason) => {
format!("error creating layer-shell backend: {reason}")
}
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
CaptureCreationError::X11(e) => format!("{e}"),
#[cfg(target_os = "macos")]
CaptureCreationError::Macos(e) => format!("{e}"),
#[cfg(windows)]
CaptureCreationError::Windows => String::new(),
CaptureCreationError::NoAvailableBackend => "no available backend".to_string(),
};
write!(f, "could not create input capture: {reason}")
}
}
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub enum LibeiCaptureCreationError {
Ashpd(#[from] ashpd::Error),
}
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
impl Display for LibeiCaptureCreationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LibeiCaptureCreationError::Ashpd(portal_error) => write!(f, "{portal_error}"),
}
}
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub struct WaylandBindError {
inner: BindError,
protocol: &'static str,
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
impl WaylandBindError {
pub(crate) fn new(inner: BindError, protocol: &'static str) -> Self {
Self { inner, protocol }
}
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
impl Display for WaylandBindError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} protocol not supported: {}",
self.protocol, self.inner
)
}
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub enum LayerShellCaptureCreationError {
Connect(#[from] ConnectError),
Global(#[from] GlobalError),
Wayland(#[from] WaylandError),
Bind(#[from] WaylandBindError),
Dispatch(#[from] DispatchError),
Io(#[from] io::Error),
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
impl Display for LayerShellCaptureCreationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LayerShellCaptureCreationError::Bind(e) => write!(f, "{e}"),
LayerShellCaptureCreationError::Connect(e) => {
write!(f, "could not connect to wayland compositor: {e}")
}
LayerShellCaptureCreationError::Global(e) => write!(f, "wayland error: {e}"),
LayerShellCaptureCreationError::Wayland(e) => write!(f, "wayland error: {e}"),
LayerShellCaptureCreationError::Dispatch(e) => {
write!(f, "error dispatching wayland events: {e}")
}
LayerShellCaptureCreationError::Io(e) => write!(f, "io error: {e}"),
}
}
}
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub enum X11InputCaptureCreationError {
NotImplemented,
}
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
impl Display for X11InputCaptureCreationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "X11 input capture is not yet implemented :(")
}
}
#[cfg(target_os = "macos")]
#[derive(Debug, Error)]
pub enum MacOSInputCaptureCreationError {
NotImplemented,
}
#[cfg(target_os = "macos")]
impl Display for MacOSInputCaptureCreationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "macos input capture is not yet implemented :(")
}
}

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

@@ -0,0 +1,159 @@
use std::{fmt::Display, io};
use futures_core::Stream;
use input_event::Event;
use self::error::CaptureCreationError;
pub mod error;
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
pub mod libei;
#[cfg(target_os = "macos")]
pub mod macos;
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
pub mod wayland;
#[cfg(windows)]
pub mod windows;
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
pub mod x11;
/// fallback input capture (does not produce events)
pub mod dummy;
pub type CaptureHandle = u64;
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
pub enum Position {
Left,
Right,
Top,
Bottom,
}
impl Position {
pub fn opposite(&self) -> Self {
match self {
Position::Left => Self::Right,
Position::Right => Self::Left,
Position::Top => Self::Bottom,
Position::Bottom => Self::Top,
}
}
}
impl Display for Position {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let pos = match self {
Position::Left => "left",
Position::Right => "right",
Position::Top => "top",
Position::Bottom => "bottom",
};
write!(f, "{}", pos)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Backend {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
InputCapturePortal,
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
LayerShell,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
X11,
#[cfg(windows)]
Windows,
#[cfg(target_os = "macos")]
MacOs,
Dummy,
}
impl Display for Backend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::InputCapturePortal => write!(f, "input-capture-portal"),
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
Backend::LayerShell => write!(f, "layer-shell"),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11 => write!(f, "X11"),
#[cfg(windows)]
Backend::Windows => write!(f, "windows"),
#[cfg(target_os = "macos")]
Backend::MacOs => write!(f, "MacOS"),
Backend::Dummy => write!(f, "dummy"),
}
}
}
pub trait InputCapture: Stream<Item = io::Result<(CaptureHandle, Event)>> + Unpin {
/// create a new client with the given id
fn create(&mut self, id: CaptureHandle, pos: Position) -> io::Result<()>;
/// destroy the client with the given id, if it exists
fn destroy(&mut self, id: CaptureHandle) -> io::Result<()>;
/// release mouse
fn release(&mut self) -> io::Result<()>;
}
pub async fn create_backend(
backend: Backend,
) -> Result<Box<dyn InputCapture<Item = io::Result<(CaptureHandle, Event)>>>, CaptureCreationError>
{
match backend {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::InputCapturePortal => Ok(Box::new(libei::LibeiInputCapture::new().await?)),
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
Backend::LayerShell => Ok(Box::new(wayland::WaylandInputCapture::new()?)),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11 => Ok(Box::new(x11::X11InputCapture::new()?)),
#[cfg(windows)]
Backend::Windows => Ok(Box::new(windows::WindowsInputCapture::new())),
#[cfg(target_os = "macos")]
Backend::MacOs => Ok(Box::new(macos::MacOSInputCapture::new()?)),
Backend::Dummy => Ok(Box::new(dummy::DummyInputCapture::new())),
}
}
pub async fn create(
backend: Option<Backend>,
) -> Result<Box<dyn InputCapture<Item = io::Result<(CaptureHandle, Event)>>>, CaptureCreationError>
{
if let Some(backend) = backend {
let b = create_backend(backend).await;
if b.is_ok() {
log::info!("using capture backend: {backend}");
}
return b;
}
for backend in [
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::InputCapturePortal,
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
Backend::LayerShell,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11,
#[cfg(windows)]
Backend::Windows,
#[cfg(target_os = "macos")]
Backend::MacOs,
Backend::Dummy,
] {
match create_backend(backend).await {
Ok(b) => {
log::info!("using capture backend: {backend}");
return Ok(b);
}
Err(e) => log::warn!("{backend} input capture backend unavailable: {e}"),
}
}
Err(CaptureCreationError::NoAvailableBackend)
}

View File

@@ -30,23 +30,24 @@ use tokio::{
use futures_core::Stream; use futures_core::Stream;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use crate::{ use input_event::{Event, KeyboardEvent, PointerEvent};
capture::InputCapture as LanMouseInputCapture,
client::{ClientEvent, ClientHandle, Position}, use super::{
event::{Event, KeyboardEvent, PointerEvent}, error::LibeiCaptureCreationError, CaptureHandle, InputCapture as LanMouseInputCapture, Position,
}; };
#[derive(Debug)] #[derive(Debug)]
enum ProducerEvent { enum ProducerEvent {
Release, Release,
ClientEvent(ClientEvent), Create(CaptureHandle, Position),
Destroy(CaptureHandle),
} }
#[allow(dead_code)] #[allow(dead_code)]
pub struct LibeiInputCapture<'a> { pub struct LibeiInputCapture<'a> {
input_capture: Pin<Box<InputCapture<'a>>>, input_capture: Pin<Box<InputCapture<'a>>>,
libei_task: JoinHandle<Result<()>>, libei_task: JoinHandle<Result<()>>,
event_rx: tokio::sync::mpsc::Receiver<(u32, Event)>, event_rx: tokio::sync::mpsc::Receiver<(CaptureHandle, Event)>,
notify_tx: tokio::sync::mpsc::Sender<ProducerEvent>, notify_tx: tokio::sync::mpsc::Sender<ProducerEvent>,
} }
@@ -79,9 +80,9 @@ fn pos_to_barrier(r: &Region, pos: Position) -> (i32, i32, i32, i32) {
fn select_barriers( fn select_barriers(
zones: &Zones, zones: &Zones,
clients: &Vec<(ClientHandle, Position)>, clients: &Vec<(CaptureHandle, Position)>,
next_barrier_id: &mut u32, next_barrier_id: &mut u32,
) -> (Vec<Barrier>, HashMap<BarrierID, ClientHandle>) { ) -> (Vec<Barrier>, HashMap<BarrierID, CaptureHandle>) {
let mut client_for_barrier = HashMap::new(); let mut client_for_barrier = HashMap::new();
let mut barriers: Vec<Barrier> = vec![]; let mut barriers: Vec<Barrier> = vec![];
@@ -105,9 +106,9 @@ fn select_barriers(
async fn update_barriers( async fn update_barriers(
input_capture: &InputCapture<'_>, input_capture: &InputCapture<'_>,
session: &Session<'_>, session: &Session<'_>,
active_clients: &Vec<(ClientHandle, Position)>, active_clients: &Vec<(CaptureHandle, Position)>,
next_barrier_id: &mut u32, next_barrier_id: &mut u32,
) -> Result<HashMap<BarrierID, ClientHandle>> { ) -> Result<HashMap<BarrierID, CaptureHandle>> {
let zones = input_capture.zones(session).await?.response()?; let zones = input_capture.zones(session).await?.response()?;
log::debug!("zones: {zones:?}"); log::debug!("zones: {zones:?}");
@@ -119,7 +120,7 @@ async fn update_barriers(
.set_pointer_barriers(session, &barriers, zones.zone_set()) .set_pointer_barriers(session, &barriers, zones.zone_set())
.await?; .await?;
let response = response.response()?; let response = response.response()?;
log::info!("{response:?}"); log::debug!("{response:?}");
Ok(id_map) Ok(id_map)
} }
@@ -131,8 +132,8 @@ impl<'a> Drop for LibeiInputCapture<'a> {
async fn create_session<'a>( async fn create_session<'a>(
input_capture: &'a InputCapture<'a>, input_capture: &'a InputCapture<'a>,
) -> Result<(Session<'a>, BitFlags<Capabilities>)> { ) -> std::result::Result<(Session<'a>, BitFlags<Capabilities>), ashpd::Error> {
log::info!("creating input capture session"); log::debug!("creating input capture session");
let (session, capabilities) = loop { let (session, capabilities) = loop {
match input_capture match input_capture
.create_session( .create_session(
@@ -154,7 +155,7 @@ async fn connect_to_eis(
input_capture: &InputCapture<'_>, input_capture: &InputCapture<'_>,
session: &Session<'_>, session: &Session<'_>,
) -> Result<(ei::Context, EiConvertEventStream)> { ) -> Result<(ei::Context, EiConvertEventStream)> {
log::info!("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?;
// create unix stream from fd // create unix stream from fd
@@ -183,8 +184,8 @@ async fn connect_to_eis(
async fn libei_event_handler( async fn libei_event_handler(
mut ei_event_stream: EiConvertEventStream, mut ei_event_stream: EiConvertEventStream,
context: ei::Context, context: ei::Context,
event_tx: Sender<(u32, Event)>, event_tx: Sender<(CaptureHandle, Event)>,
current_client: Rc<Cell<Option<ClientHandle>>>, current_client: Rc<Cell<Option<CaptureHandle>>>,
) -> Result<()> { ) -> Result<()> {
loop { loop {
let ei_event = match ei_event_stream.next().await { let ei_event = match ei_event_stream.next().await {
@@ -200,12 +201,12 @@ async fn libei_event_handler(
async fn wait_for_active_client( async fn wait_for_active_client(
notify_rx: &mut Receiver<ProducerEvent>, notify_rx: &mut Receiver<ProducerEvent>,
active_clients: &mut Vec<(ClientHandle, Position)>, active_clients: &mut Vec<(CaptureHandle, Position)>,
) -> Result<()> { ) -> Result<()> {
// wait for a client update // wait for a client update
while let Some(producer_event) = notify_rx.recv().await { while let Some(producer_event) = notify_rx.recv().await {
if let ProducerEvent::ClientEvent(c) = producer_event { if let ProducerEvent::Create(c, p) = producer_event {
handle_producer_event(ProducerEvent::ClientEvent(c), active_clients)?; handle_producer_event(ProducerEvent::Create(c, p), active_clients)?;
break; break;
} }
} }
@@ -213,7 +214,7 @@ async fn wait_for_active_client(
} }
impl<'a> LibeiInputCapture<'a> { impl<'a> LibeiInputCapture<'a> {
pub async fn new() -> Result<Self> { pub async fn new() -> std::result::Result<Self, LibeiCaptureCreationError> {
let input_capture = Box::pin(InputCapture::new().await?); let input_capture = Box::pin(InputCapture::new().await?);
let input_capture_ptr = input_capture.as_ref().get_ref() as *const InputCapture<'static>; let input_capture_ptr = input_capture.as_ref().get_ref() as *const InputCapture<'static>;
let mut first_session = Some(create_session(unsafe { &*input_capture_ptr }).await?); let mut first_session = Some(create_session(unsafe { &*input_capture_ptr }).await?);
@@ -224,7 +225,7 @@ impl<'a> LibeiInputCapture<'a> {
/* safety: libei_task does not outlive Self */ /* safety: libei_task does not outlive Self */
let input_capture = unsafe { &*input_capture_ptr }; let input_capture = unsafe { &*input_capture_ptr };
let mut active_clients: Vec<(ClientHandle, Position)> = vec![]; let mut active_clients: Vec<(CaptureHandle, Position)> = vec![];
let mut next_barrier_id = 1u32; let mut next_barrier_id = 1u32;
/* there is a bug in xdg-remote-desktop-portal-gnome / mutter that /* there is a bug in xdg-remote-desktop-portal-gnome / mutter that
@@ -270,7 +271,7 @@ impl<'a> LibeiInputCapture<'a> {
) )
.await?; .await?;
log::info!("enabling session"); log::debug!("enabling session");
input_capture.enable(&session).await?; input_capture.enable(&session).await?;
loop { loop {
@@ -347,8 +348,8 @@ async fn release_capture(
input_capture: &InputCapture<'_>, input_capture: &InputCapture<'_>,
session: &Session<'_>, session: &Session<'_>,
activated: Activated, activated: Activated,
current_client: ClientHandle, current_client: CaptureHandle,
active_clients: &[(ClientHandle, Position)], active_clients: &[(CaptureHandle, Position)],
) -> Result<()> { ) -> Result<()> {
log::debug!("releasing input capture {}", activated.activation_id()); log::debug!("releasing input capture {}", activated.activation_id());
let (x, y) = activated.cursor_position(); let (x, y) = activated.cursor_position();
@@ -375,16 +376,16 @@ async fn release_capture(
fn handle_producer_event( fn handle_producer_event(
producer_event: ProducerEvent, producer_event: ProducerEvent,
active_clients: &mut Vec<(ClientHandle, Position)>, active_clients: &mut Vec<(CaptureHandle, Position)>,
) -> Result<bool> { ) -> Result<bool> {
log::debug!("handling event: {producer_event:?}"); log::debug!("handling event: {producer_event:?}");
let updated = match producer_event { let updated = match producer_event {
ProducerEvent::Release => false, ProducerEvent::Release => false,
ProducerEvent::ClientEvent(ClientEvent::Create(c, p)) => { ProducerEvent::Create(c, p) => {
active_clients.push((c, p)); active_clients.push((c, p));
true true
} }
ProducerEvent::ClientEvent(ClientEvent::Destroy(c)) => { ProducerEvent::Destroy(c) => {
active_clients.retain(|(h, _)| *h != c); active_clients.retain(|(h, _)| *h != c);
true true
} }
@@ -394,9 +395,9 @@ fn handle_producer_event(
async fn handle_ei_event( async fn handle_ei_event(
ei_event: EiEvent, ei_event: EiEvent,
current_client: Option<ClientHandle>, current_client: Option<CaptureHandle>,
context: &ei::Context, context: &ei::Context,
event_tx: &Sender<(u32, Event)>, event_tx: &Sender<(CaptureHandle, Event)>,
) { ) {
match ei_event { match ei_event {
EiEvent::SeatAdded(s) => { EiEvent::SeatAdded(s) => {
@@ -466,15 +467,38 @@ async fn handle_ei_event(
.unwrap(); .unwrap();
} }
} }
EiEvent::ScrollDelta(_) => {} EiEvent::ScrollDelta(delta) => {
if let Some(handle) = current_client {
let mut events = vec![];
if delta.dy != 0. {
events.push(PointerEvent::Axis {
time: 0,
axis: 0,
value: delta.dy as f64,
});
}
if delta.dx != 0. {
events.push(PointerEvent::Axis {
time: 0,
axis: 1,
value: delta.dx as f64,
});
}
for event in events {
event_tx
.send((handle, Event::Pointer(event)))
.await
.unwrap();
}
}
}
EiEvent::ScrollStop(_) => {} EiEvent::ScrollStop(_) => {}
EiEvent::ScrollCancel(_) => {} EiEvent::ScrollCancel(_) => {}
EiEvent::ScrollDiscrete(scroll) => { EiEvent::ScrollDiscrete(scroll) => {
if scroll.discrete_dy != 0 { if scroll.discrete_dy != 0 {
let event = PointerEvent::Axis { let event = PointerEvent::AxisDiscrete120 {
time: 0,
axis: 0, axis: 0,
value: scroll.discrete_dy as f64, value: scroll.discrete_dy,
}; };
if let Some(current_client) = current_client { if let Some(current_client) = current_client {
event_tx event_tx
@@ -484,10 +508,9 @@ async fn handle_ei_event(
} }
} }
if scroll.discrete_dx != 0 { if scroll.discrete_dx != 0 {
let event = PointerEvent::Axis { let event = PointerEvent::AxisDiscrete120 {
time: 0,
axis: 1, axis: 1,
value: scroll.discrete_dx as f64, value: scroll.discrete_dx,
}; };
if let Some(current_client) = current_client { if let Some(current_client) = current_client {
event_tx event_tx
@@ -523,12 +546,18 @@ async fn handle_ei_event(
} }
impl<'a> LanMouseInputCapture for LibeiInputCapture<'a> { impl<'a> LanMouseInputCapture for LibeiInputCapture<'a> {
fn notify(&mut self, event: ClientEvent) -> io::Result<()> { fn create(&mut self, handle: super::CaptureHandle, pos: super::Position) -> io::Result<()> {
let notify_tx = self.notify_tx.clone(); let notify_tx = self.notify_tx.clone();
tokio::task::spawn_local(async move { tokio::task::spawn_local(async move {
log::debug!("notifying {event:?}"); let _ = notify_tx.send(ProducerEvent::Create(handle, pos)).await;
let _ = notify_tx.send(ProducerEvent::ClientEvent(event)).await; });
log::debug!("done !"); Ok(())
}
fn destroy(&mut self, handle: super::CaptureHandle) -> io::Result<()> {
let notify_tx = self.notify_tx.clone();
tokio::task::spawn_local(async move {
let _ = notify_tx.send(ProducerEvent::Destroy(handle)).await;
}); });
Ok(()) Ok(())
} }
@@ -536,7 +565,6 @@ impl<'a> LanMouseInputCapture for LibeiInputCapture<'a> {
fn release(&mut self) -> io::Result<()> { fn release(&mut self) -> io::Result<()> {
let notify_tx = self.notify_tx.clone(); let notify_tx = self.notify_tx.clone();
tokio::task::spawn_local(async move { tokio::task::spawn_local(async move {
log::debug!("notifying Release");
let _ = notify_tx.send(ProducerEvent::Release).await; let _ = notify_tx.send(ProducerEvent::Release).await;
}); });
Ok(()) Ok(())
@@ -544,7 +572,7 @@ impl<'a> LanMouseInputCapture for LibeiInputCapture<'a> {
} }
impl<'a> Stream for LibeiInputCapture<'a> { impl<'a> Stream for LibeiInputCapture<'a> {
type Item = io::Result<(ClientHandle, Event)>; type Item = io::Result<(CaptureHandle, Event)>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match ready!(self.event_rx.poll_recv(cx)) { match ready!(self.event_rx.poll_recv(cx)) {

View File

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

View File

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

View File

@@ -0,0 +1,630 @@
use anyhow::Result;
use core::task::{Context, Poll};
use futures::Stream;
use once_cell::unsync::Lazy;
use std::collections::HashMap;
use std::ptr::{addr_of, addr_of_mut};
use futures::executor::block_on;
use std::default::Default;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{mpsc, Mutex};
use std::task::ready;
use std::{io, pin::Pin, thread};
use tokio::sync::mpsc::{channel, Receiver, Sender};
use windows::core::{w, PCWSTR};
use windows::Win32::Foundation::{FALSE, HINSTANCE, HWND, LPARAM, LRESULT, RECT, WPARAM};
use windows::Win32::Graphics::Gdi::{
EnumDisplayDevicesW, EnumDisplaySettingsW, DEVMODEW, DISPLAY_DEVICEW,
DISPLAY_DEVICE_ATTACHED_TO_DESKTOP, ENUM_CURRENT_SETTINGS,
};
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::System::Threading::GetCurrentThreadId;
use windows::Win32::UI::WindowsAndMessaging::{
CallNextHookEx, CreateWindowExW, DispatchMessageW, GetMessageW, PostThreadMessageW,
RegisterClassW, SetWindowsHookExW, TranslateMessage, EDD_GET_DEVICE_INTERFACE_NAME, HHOOK,
HMENU, HOOKPROC, KBDLLHOOKSTRUCT, LLKHF_EXTENDED, MSG, MSLLHOOKSTRUCT, WH_KEYBOARD_LL,
WH_MOUSE_LL, WINDOW_STYLE, WM_DISPLAYCHANGE, WM_KEYDOWN, WM_KEYUP, WM_LBUTTONDOWN,
WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_RBUTTONDOWN,
WM_RBUTTONUP, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_USER, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSW,
WNDPROC,
};
use input_event::{
scancode::{self, Linux},
Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT,
};
use super::{CaptureHandle, InputCapture, Position};
enum Request {
Create(CaptureHandle, Position),
Destroy(CaptureHandle),
}
pub struct WindowsInputCapture {
event_rx: Receiver<(CaptureHandle, Event)>,
msg_thread: Option<std::thread::JoinHandle<()>>,
}
enum EventType {
Request = 0,
Release = 1,
Exit = 2,
}
unsafe fn signal_message_thread(event_type: EventType) {
if let Some(event_tid) = get_event_tid() {
PostThreadMessageW(event_tid, WM_USER, WPARAM(event_type as usize), LPARAM(0)).unwrap();
} else {
panic!();
}
}
impl InputCapture for WindowsInputCapture {
fn create(&mut self, handle: CaptureHandle, pos: Position) -> io::Result<()> {
unsafe {
{
let mut requests = REQUEST_BUFFER.lock().unwrap();
requests.push(Request::Create(handle, pos));
}
signal_message_thread(EventType::Request);
}
Ok(())
}
fn destroy(&mut self, handle: CaptureHandle) -> io::Result<()> {
unsafe {
{
let mut requests = REQUEST_BUFFER.lock().unwrap();
requests.push(Request::Destroy(handle));
}
signal_message_thread(EventType::Request);
}
Ok(())
}
fn release(&mut self) -> io::Result<()> {
unsafe { signal_message_thread(EventType::Release) };
Ok(())
}
}
static mut REQUEST_BUFFER: Mutex<Vec<Request>> = Mutex::new(Vec::new());
static mut ACTIVE_CLIENT: Option<CaptureHandle> = None;
static mut CLIENT_FOR_POS: Lazy<HashMap<Position, CaptureHandle>> = Lazy::new(HashMap::new);
static mut EVENT_TX: Option<Sender<(CaptureHandle, Event)>> = None;
static mut EVENT_THREAD_ID: AtomicU32 = AtomicU32::new(0);
unsafe fn set_event_tid(tid: u32) {
EVENT_THREAD_ID.store(tid, Ordering::SeqCst);
}
unsafe fn get_event_tid() -> Option<u32> {
match EVENT_THREAD_ID.load(Ordering::SeqCst) {
0 => None,
id => Some(id),
}
}
static mut ENTRY_POINT: (i32, i32) = (0, 0);
fn to_mouse_event(wparam: WPARAM, lparam: LPARAM) -> Option<PointerEvent> {
let mouse_low_level: MSLLHOOKSTRUCT = unsafe { *(lparam.0 as *const MSLLHOOKSTRUCT) };
match wparam {
WPARAM(p) if p == WM_LBUTTONDOWN as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_LEFT,
state: 1,
}),
WPARAM(p) if p == WM_MBUTTONDOWN as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_MIDDLE,
state: 1,
}),
WPARAM(p) if p == WM_RBUTTONDOWN as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_RIGHT,
state: 1,
}),
WPARAM(p) if p == WM_LBUTTONUP as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_LEFT,
state: 0,
}),
WPARAM(p) if p == WM_MBUTTONUP as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_MIDDLE,
state: 0,
}),
WPARAM(p) if p == WM_RBUTTONUP as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_RIGHT,
state: 0,
}),
WPARAM(p) if p == WM_MOUSEMOVE as usize => unsafe {
let (x, y) = (mouse_low_level.pt.x, mouse_low_level.pt.y);
let (ex, ey) = ENTRY_POINT;
let (dx, dy) = (x - ex, y - ey);
Some(PointerEvent::Motion {
time: 0,
relative_x: dx as f64,
relative_y: dy as f64,
})
},
WPARAM(p) if p == WM_MOUSEWHEEL as usize => Some(PointerEvent::AxisDiscrete120 {
axis: 0,
value: -(mouse_low_level.mouseData as i32 >> 16),
}),
WPARAM(p) if p == WM_XBUTTONDOWN as usize || p == WM_XBUTTONUP as usize => {
let hb = mouse_low_level.mouseData >> 16;
let button = match hb {
1 => BTN_BACK,
2 => BTN_FORWARD,
_ => {
log::warn!("unknown mouse button");
return None;
}
};
Some(PointerEvent::Button {
time: 0,
button,
state: if p == WM_XBUTTONDOWN as usize { 1 } else { 0 },
})
}
w => {
log::warn!("unknown mouse event: {w:?}");
None
}
}
}
unsafe fn to_key_event(wparam: WPARAM, lparam: LPARAM) -> Option<KeyboardEvent> {
let kybrdllhookstruct: KBDLLHOOKSTRUCT = *(lparam.0 as *const KBDLLHOOKSTRUCT);
let mut scan_code = kybrdllhookstruct.scanCode;
log::trace!("scan_code: {scan_code}");
if kybrdllhookstruct.flags.contains(LLKHF_EXTENDED) {
scan_code |= 0xE000;
}
let Ok(win_scan_code) = scancode::Windows::try_from(scan_code) else {
log::warn!("failed to translate to windows scancode: {scan_code}");
return None;
};
log::trace!("windows_scan: {win_scan_code:?}");
let Ok(linux_scan_code): Result<Linux, ()> = win_scan_code.try_into() else {
log::warn!("failed to translate into linux scancode: {win_scan_code:?}");
return None;
};
log::trace!("windows_scan: {linux_scan_code:?}");
let scan_code = linux_scan_code as u32;
match wparam {
WPARAM(p) if p == WM_KEYDOWN as usize => Some(KeyboardEvent::Key {
time: 0,
key: scan_code,
state: 1,
}),
WPARAM(p) if p == WM_KEYUP as usize => Some(KeyboardEvent::Key {
time: 0,
key: scan_code,
state: 0,
}),
WPARAM(p) if p == WM_SYSKEYDOWN as usize => Some(KeyboardEvent::Key {
time: 0,
key: scan_code,
state: 1,
}),
WPARAM(p) if p == WM_SYSKEYUP as usize => Some(KeyboardEvent::Key {
time: 0,
key: scan_code,
state: 1,
}),
_ => None,
}
}
///
/// clamp point to display bounds
///
/// # Arguments
///
/// * `prev_point`: coordinates, the cursor was before entering, within bounds of a display
/// * `entry_point`: point to clamp
///
/// returns: (i32, i32), the corrected entry point
///
fn clamp_to_display_bounds(prev_point: (i32, i32), point: (i32, i32)) -> (i32, i32) {
/* find display where movement came from */
let display_regions = unsafe { get_display_regions() };
let display = display_regions
.iter()
.find(|&d| is_within_dp_region(prev_point, d))
.unwrap();
/* clamp to bounds (inclusive) */
let (x, y) = point;
let (min_x, max_x) = (display.left, display.right - 1);
let (min_y, max_y) = (display.top, display.bottom - 1);
(x.clamp(min_x, max_x), y.clamp(min_y, max_y))
}
unsafe fn send_blocking(event: Event) {
if let Some(active) = ACTIVE_CLIENT {
block_on(async move {
let _ = EVENT_TX.as_ref().unwrap().send((active, event)).await;
});
}
}
unsafe fn check_client_activation(wparam: WPARAM, lparam: LPARAM) -> bool {
if wparam.0 != WM_MOUSEMOVE as usize {
return ACTIVE_CLIENT.is_some();
}
let mouse_low_level: MSLLHOOKSTRUCT = *(lparam.0 as *const MSLLHOOKSTRUCT);
static mut PREV_POS: Option<(i32, i32)> = None;
let curr_pos = (mouse_low_level.pt.x, mouse_low_level.pt.y);
let prev_pos = PREV_POS.unwrap_or(curr_pos);
PREV_POS.replace(curr_pos);
/* next event is the first actual event */
let ret = ACTIVE_CLIENT.is_some();
/* client already active, no need to check */
if ACTIVE_CLIENT.is_some() {
return ret;
}
/* check if a client was activated */
let Some(pos) = entered_barrier(prev_pos, curr_pos, get_display_regions()) else {
return ret;
};
/* check if a client is registered for the barrier */
let Some(client) = CLIENT_FOR_POS.get(&pos) else {
return ret;
};
/* update active client and entry point */
ACTIVE_CLIENT.replace(*client);
ENTRY_POINT = clamp_to_display_bounds(prev_pos, curr_pos);
/* notify main thread */
log::debug!("ENTERED @ {prev_pos:?} -> {curr_pos:?}");
send_blocking(Event::Enter());
ret
}
unsafe extern "system" fn mouse_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
let active = check_client_activation(wparam, lparam);
/* no client was active */
if !active {
return CallNextHookEx(HHOOK::default(), ncode, wparam, lparam);
}
/* get active client if any */
let Some(client) = ACTIVE_CLIENT else {
return LRESULT(1);
};
/* convert to lan-mouse event */
let Some(pointer_event) = to_mouse_event(wparam, lparam) else {
return LRESULT(1);
};
let event = (client, Event::Pointer(pointer_event));
/* notify mainthread (drop events if sending too fast) */
if let Err(e) = EVENT_TX.as_ref().unwrap().try_send(event) {
log::warn!("e: {e}");
}
/* don't pass event to applications */
LRESULT(1)
}
unsafe extern "system" fn kybrd_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
/* get active client if any */
let Some(client) = ACTIVE_CLIENT else {
return CallNextHookEx(HHOOK::default(), ncode, wparam, lparam);
};
/* convert to key event */
let Some(key_event) = to_key_event(wparam, lparam) else {
return LRESULT(1);
};
let event = (client, Event::Keyboard(key_event));
if let Err(e) = EVENT_TX.as_ref().unwrap().try_send(event) {
log::warn!("e: {e}");
}
/* don't pass event to applications */
LRESULT(1)
}
unsafe extern "system" fn window_proc(
_hwnd: HWND,
uint: u32,
_wparam: WPARAM,
_lparam: LPARAM,
) -> LRESULT {
match uint {
x if x == WM_DISPLAYCHANGE => {
log::debug!("display resolution changed");
DISPLAY_RESOLUTION_CHANGED = true;
}
_ => {}
}
LRESULT(1)
}
fn enumerate_displays() -> Vec<RECT> {
unsafe {
let mut display_rects = vec![];
let mut devices = vec![];
for i in 0.. {
let mut device: DISPLAY_DEVICEW = std::mem::zeroed();
device.cb = std::mem::size_of::<DISPLAY_DEVICEW>() as u32;
let ret = EnumDisplayDevicesW(None, i, &mut device, EDD_GET_DEVICE_INTERFACE_NAME);
if ret == FALSE {
break;
}
if device.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP != 0 {
devices.push(device.DeviceName);
}
}
for device in devices {
let mut dev_mode: DEVMODEW = std::mem::zeroed();
dev_mode.dmSize = std::mem::size_of::<DEVMODEW>() as u16;
let ret = EnumDisplaySettingsW(
PCWSTR::from_raw(&device as *const _),
ENUM_CURRENT_SETTINGS,
&mut dev_mode,
);
if ret == FALSE {
log::warn!("no display mode");
}
let pos = dev_mode.Anonymous1.Anonymous2.dmPosition;
let (x, y) = (pos.x, pos.y);
let (width, height) = (dev_mode.dmPelsWidth, dev_mode.dmPelsHeight);
display_rects.push(RECT {
left: x,
right: x + width as i32,
top: y,
bottom: y + height as i32,
});
}
display_rects
}
}
static mut DISPLAY_RESOLUTION_CHANGED: bool = true;
unsafe fn get_display_regions() -> &'static Vec<RECT> {
static mut DISPLAYS: Vec<RECT> = vec![];
if DISPLAY_RESOLUTION_CHANGED {
DISPLAYS = enumerate_displays();
DISPLAY_RESOLUTION_CHANGED = false;
log::debug!("displays: {DISPLAYS:?}");
}
&*addr_of!(DISPLAYS)
}
fn is_within_dp_region(point: (i32, i32), display: &RECT) -> bool {
[
Position::Left,
Position::Right,
Position::Top,
Position::Bottom,
]
.iter()
.all(|&pos| is_within_dp_boundary(point, display, pos))
}
fn is_within_dp_boundary(point: (i32, i32), display: &RECT, pos: Position) -> bool {
let (x, y) = point;
match pos {
Position::Left => display.left <= x,
Position::Right => display.right > x,
Position::Top => display.top <= y,
Position::Bottom => display.bottom > y,
}
}
/// returns whether the given position is within the display bounds with respect to the given
/// barrier position
///
/// # Arguments
///
/// * `x`:
/// * `y`:
/// * `displays`:
/// * `pos`:
///
/// returns: bool
///
fn in_bounds(point: (i32, i32), displays: &[RECT], pos: Position) -> bool {
displays
.iter()
.any(|d| is_within_dp_boundary(point, d, pos))
}
fn in_display_region(point: (i32, i32), displays: &[RECT]) -> bool {
displays.iter().any(|d| is_within_dp_region(point, d))
}
fn moved_across_boundary(
prev_pos: (i32, i32),
curr_pos: (i32, i32),
displays: &[RECT],
pos: Position,
) -> bool {
/* was within bounds, but is not anymore */
in_display_region(prev_pos, displays) && !in_bounds(curr_pos, displays, pos)
}
fn entered_barrier(
prev_pos: (i32, i32),
curr_pos: (i32, i32),
displays: &[RECT],
) -> Option<Position> {
[
Position::Left,
Position::Right,
Position::Top,
Position::Bottom,
]
.into_iter()
.find(|&pos| moved_across_boundary(prev_pos, curr_pos, displays, pos))
}
fn get_msg() -> Option<MSG> {
unsafe {
let mut msg = std::mem::zeroed();
let ret = GetMessageW(addr_of_mut!(msg), HWND::default(), 0, 0);
match ret.0 {
0 => None,
x if x > 0 => Some(msg),
_ => panic!("error in GetMessageW"),
}
}
}
fn message_thread(ready_tx: mpsc::Sender<()>) {
unsafe {
set_event_tid(GetCurrentThreadId());
ready_tx.send(()).expect("channel closed");
let mouse_proc: HOOKPROC = Some(mouse_proc);
let kybrd_proc: HOOKPROC = Some(kybrd_proc);
let window_proc: WNDPROC = Some(window_proc);
/* register hooks */
let _ = SetWindowsHookExW(WH_MOUSE_LL, mouse_proc, HINSTANCE::default(), 0).unwrap();
let _ = SetWindowsHookExW(WH_KEYBOARD_LL, kybrd_proc, HINSTANCE::default(), 0).unwrap();
let instance = GetModuleHandleW(None).unwrap();
let window_class: WNDCLASSW = WNDCLASSW {
lpfnWndProc: window_proc,
hInstance: instance.into(),
lpszClassName: w!("lan-mouse-message-window-class"),
..Default::default()
};
let ret = RegisterClassW(&window_class);
if ret == 0 {
panic!("RegisterClassW");
}
/* window is used ro receive WM_DISPLAYCHANGE messages */
let ret = CreateWindowExW(
Default::default(),
w!("lan-mouse-message-window-class"),
w!("lan-mouse-msg-window"),
WINDOW_STYLE::default(),
0,
0,
0,
0,
HWND::default(),
HMENU::default(),
instance,
None,
);
if ret.0 == 0 {
panic!("CreateWindowExW");
}
/* run message loop */
loop {
// mouse / keybrd proc do not actually return a message
let Some(msg) = get_msg() else {
break;
};
if msg.hwnd.0 == 0 {
/* messages sent via PostThreadMessage */
match msg.wParam.0 {
x if x == EventType::Exit as usize => break,
x if x == EventType::Release as usize => {
ACTIVE_CLIENT.take();
}
x if x == EventType::Request as usize => {
let requests = {
let mut res = vec![];
let mut requests = REQUEST_BUFFER.lock().unwrap();
for request in requests.drain(..) {
res.push(request);
}
res
};
for request in requests {
update_clients(request)
}
}
_ => {}
}
} else {
/* other messages for window_procs */
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
}
}
fn update_clients(request: Request) {
match request {
Request::Create(handle, pos) => {
unsafe { CLIENT_FOR_POS.insert(pos, handle) };
}
Request::Destroy(handle) => unsafe {
for pos in [
Position::Left,
Position::Right,
Position::Top,
Position::Bottom,
] {
if ACTIVE_CLIENT == Some(handle) {
ACTIVE_CLIENT.take();
}
if CLIENT_FOR_POS.get(&pos).copied() == Some(handle) {
CLIENT_FOR_POS.remove(&pos);
}
}
},
}
}
impl WindowsInputCapture {
pub(crate) fn new() -> Self {
unsafe {
let (tx, rx) = channel(10);
EVENT_TX.replace(tx);
let (ready_tx, ready_rx) = mpsc::channel();
let msg_thread = Some(thread::spawn(|| message_thread(ready_tx)));
/* wait for thread to set its id */
ready_rx.recv().expect("channel closed");
Self {
msg_thread,
event_rx: rx,
}
}
}
}
impl Stream for WindowsInputCapture {
type Item = io::Result<(CaptureHandle, Event)>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match ready!(self.event_rx.poll_recv(cx)) {
None => Poll::Ready(None),
Some(e) => Poll::Ready(Some(Ok(e))),
}
}
}
impl Drop for WindowsInputCapture {
fn drop(&mut self) {
unsafe { signal_message_thread(EventType::Exit) };
let _ = self.msg_thread.take().unwrap().join();
}
}

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

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

View File

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

View File

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

View File

@@ -0,0 +1,192 @@
use std::{fmt::Display, io};
use thiserror::Error;
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
use wayland_client::{
backend::WaylandError,
globals::{BindError, GlobalError},
ConnectError, DispatchError,
};
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
use reis::tokio::HandshakeError;
#[derive(Debug, Error)]
pub enum EmulationError {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("libei error flushing events: `{0}`")]
Libei(#[from] reis::event::Error),
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[error("wayland error: `{0}`")]
Wayland(#[from] wayland_client::backend::WaylandError),
#[cfg(all(
unix,
any(feature = "xdg_desktop_portal", feature = "libei"),
not(target_os = "macos")
))]
#[error("xdg-desktop-portal: `{0}`")]
Ashpd(#[from] ashpd::Error),
#[error("io error: `{0}`")]
Io(#[from] io::Error),
}
#[derive(Debug, Error)]
pub enum EmulationCreationError {
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
Wlroots(#[from] WlrootsEmulationCreationError),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Libei(#[from] LibeiEmulationCreationError),
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
Xdp(#[from] XdpEmulationCreationError),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
X11(#[from] X11EmulationCreationError),
#[cfg(target_os = "macos")]
MacOs(#[from] MacOSEmulationCreationError),
#[cfg(windows)]
Windows(#[from] WindowsEmulationCreationError),
NoAvailableBackend,
}
impl Display for EmulationCreationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let reason = match self {
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
EmulationCreationError::Wlroots(e) => format!("wlroots backend: {e}"),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
EmulationCreationError::Libei(e) => format!("libei backend: {e}"),
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
EmulationCreationError::Xdp(e) => format!("desktop portal backend: {e}"),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
EmulationCreationError::X11(e) => format!("x11 backend: {e}"),
#[cfg(target_os = "macos")]
EmulationCreationError::MacOs(e) => format!("macos backend: {e}"),
#[cfg(windows)]
EmulationCreationError::Windows(e) => format!("windows backend: {e}"),
EmulationCreationError::NoAvailableBackend => "no backend available".to_string(),
};
write!(f, "could not create input emulation backend: {reason}")
}
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub enum WlrootsEmulationCreationError {
Connect(#[from] ConnectError),
Global(#[from] GlobalError),
Wayland(#[from] WaylandError),
Bind(#[from] WaylandBindError),
Dispatch(#[from] DispatchError),
Io(#[from] std::io::Error),
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub struct WaylandBindError {
inner: BindError,
protocol: &'static str,
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
impl WaylandBindError {
pub(crate) fn new(inner: BindError, protocol: &'static str) -> Self {
Self { inner, protocol }
}
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
impl Display for WaylandBindError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} protocol not supported: {}",
self.protocol, self.inner
)
}
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
impl Display for WlrootsEmulationCreationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WlrootsEmulationCreationError::Bind(e) => write!(f, "{e}"),
WlrootsEmulationCreationError::Connect(e) => {
write!(f, "could not connect to wayland compositor: {e}")
}
WlrootsEmulationCreationError::Global(e) => write!(f, "wayland error: {e}"),
WlrootsEmulationCreationError::Wayland(e) => write!(f, "wayland error: {e}"),
WlrootsEmulationCreationError::Dispatch(e) => {
write!(f, "error dispatching wayland events: {e}")
}
WlrootsEmulationCreationError::Io(e) => write!(f, "io error: {e}"),
}
}
}
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub enum LibeiEmulationCreationError {
Ashpd(#[from] ashpd::Error),
Io(#[from] std::io::Error),
Handshake(#[from] HandshakeError),
}
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
impl Display for LibeiEmulationCreationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LibeiEmulationCreationError::Ashpd(e) => write!(f, "xdg-desktop-portal: {e}"),
LibeiEmulationCreationError::Io(e) => write!(f, "io error: {e}"),
LibeiEmulationCreationError::Handshake(e) => write!(f, "error in libei handshake: {e}"),
}
}
}
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub enum XdpEmulationCreationError {
Ashpd(#[from] ashpd::Error),
}
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
impl Display for XdpEmulationCreationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
XdpEmulationCreationError::Ashpd(e) => write!(f, "portal error: {e}"),
}
}
}
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub enum X11EmulationCreationError {
OpenDisplay,
}
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
impl Display for X11EmulationCreationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
X11EmulationCreationError::OpenDisplay => write!(f, "could not open display!"),
}
}
}
#[cfg(target_os = "macos")]
#[derive(Debug, Error)]
pub enum MacOSEmulationCreationError {
EventSourceCreation,
}
#[cfg(target_os = "macos")]
impl Display for MacOSEmulationCreationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MacOSEmulationCreationError::EventSourceCreation => {
write!(f, "could not create event source")
}
}
}
}
#[cfg(windows)]
#[derive(Debug, Error)]
pub enum WindowsEmulationCreationError {}

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

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

View File

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

View File

@@ -1,18 +1,18 @@
use crate::client::{ClientEvent, ClientHandle}; use super::{error::EmulationError, EmulationHandle, InputEmulation};
use crate::emulate::InputEmulation;
use crate::event::{Event, KeyboardEvent, PointerEvent};
use anyhow::{anyhow, Result};
use async_trait::async_trait; use async_trait::async_trait;
use core_graphics::display::{CGDisplayBounds, CGMainDisplayID, CGPoint}; use core_graphics::display::{CGDisplayBounds, CGMainDisplayID, CGPoint};
use core_graphics::event::{ use core_graphics::event::{
CGEvent, CGEventTapLocation, CGEventType, CGKeyCode, CGMouseButton, EventField, ScrollEventUnit, CGEvent, CGEventTapLocation, CGEventType, CGKeyCode, CGMouseButton, EventField, ScrollEventUnit,
}; };
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID}; use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
use input_event::{Event, KeyboardEvent, PointerEvent};
use keycode::{KeyMap, KeyMapping}; use keycode::{KeyMap, KeyMapping};
use std::ops::{Index, IndexMut}; use std::ops::{Index, IndexMut};
use std::time::Duration; use std::time::Duration;
use tokio::task::AbortHandle; use tokio::task::AbortHandle;
use super::error::MacOSEmulationCreationError;
const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500); const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500);
const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32); const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32);
@@ -53,11 +53,9 @@ impl IndexMut<CGMouseButton> for ButtonState {
unsafe impl Send for MacOSEmulation {} unsafe impl Send for MacOSEmulation {}
impl MacOSEmulation { impl MacOSEmulation {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self, MacOSEmulationCreationError> {
let event_source = match CGEventSource::new(CGEventSourceStateID::CombinedSessionState) { let event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState)
Ok(e) => e, .map_err(|_| MacOSEmulationCreationError::EventSourceCreation)?;
Err(_) => return Err(anyhow!("event source creation failed!")),
};
let button_state = ButtonState { let button_state = ButtonState {
left: false, left: false,
right: false, right: false,
@@ -109,7 +107,11 @@ fn key_event(event_source: CGEventSource, key: u16, state: u8) {
#[async_trait] #[async_trait]
impl InputEmulation for MacOSEmulation { impl InputEmulation for MacOSEmulation {
async fn consume(&mut self, event: Event, _client_handle: ClientHandle) { async fn consume(
&mut self,
event: Event,
_handle: EmulationHandle,
) -> Result<(), EmulationError> {
match event { match event {
Event::Pointer(pointer_event) => match pointer_event { Event::Pointer(pointer_event) => match pointer_event {
PointerEvent::Motion { PointerEvent::Motion {
@@ -131,7 +133,7 @@ impl InputEmulation for MacOSEmulation {
Some(l) => l, Some(l) => l,
None => { None => {
log::warn!("could not get mouse location!"); log::warn!("could not get mouse location!");
return; return Ok(());
} }
}; };
@@ -155,7 +157,7 @@ impl InputEmulation for MacOSEmulation {
Ok(e) => e, Ok(e) => e,
Err(_) => { Err(_) => {
log::warn!("mouse event creation failed!"); log::warn!("mouse event creation failed!");
return; return Ok(());
} }
}; };
event.set_integer_value_field( event.set_integer_value_field(
@@ -174,27 +176,27 @@ impl InputEmulation for MacOSEmulation {
state, state,
} => { } => {
let (event_type, mouse_button) = match (button, state) { let (event_type, mouse_button) = match (button, state) {
(b, 1) if b == crate::event::BTN_LEFT => { (b, 1) if b == input_event::BTN_LEFT => {
(CGEventType::LeftMouseDown, CGMouseButton::Left) (CGEventType::LeftMouseDown, CGMouseButton::Left)
} }
(b, 0) if b == crate::event::BTN_LEFT => { (b, 0) if b == input_event::BTN_LEFT => {
(CGEventType::LeftMouseUp, CGMouseButton::Left) (CGEventType::LeftMouseUp, CGMouseButton::Left)
} }
(b, 1) if b == crate::event::BTN_RIGHT => { (b, 1) if b == input_event::BTN_RIGHT => {
(CGEventType::RightMouseDown, CGMouseButton::Right) (CGEventType::RightMouseDown, CGMouseButton::Right)
} }
(b, 0) if b == crate::event::BTN_RIGHT => { (b, 0) if b == input_event::BTN_RIGHT => {
(CGEventType::RightMouseUp, CGMouseButton::Right) (CGEventType::RightMouseUp, CGMouseButton::Right)
} }
(b, 1) if b == crate::event::BTN_MIDDLE => { (b, 1) if b == input_event::BTN_MIDDLE => {
(CGEventType::OtherMouseDown, CGMouseButton::Center) (CGEventType::OtherMouseDown, CGMouseButton::Center)
} }
(b, 0) if b == crate::event::BTN_MIDDLE => { (b, 0) if b == input_event::BTN_MIDDLE => {
(CGEventType::OtherMouseUp, CGMouseButton::Center) (CGEventType::OtherMouseUp, CGMouseButton::Center)
} }
_ => { _ => {
log::warn!("invalid button event: {button},{state}"); log::warn!("invalid button event: {button},{state}");
return; return Ok(());
} }
}; };
// store button state // store button state
@@ -210,7 +212,7 @@ impl InputEmulation for MacOSEmulation {
Ok(e) => e, Ok(e) => e,
Err(()) => { Err(()) => {
log::warn!("mouse event creation failed!"); log::warn!("mouse event creation failed!");
return; return Ok(());
} }
}; };
event.post(CGEventTapLocation::HID); event.post(CGEventTapLocation::HID);
@@ -220,18 +222,18 @@ impl InputEmulation for MacOSEmulation {
axis, axis,
value, value,
} => { } => {
let value = value as i32 / 10; // FIXME: high precision scroll events let value = value as i32;
let (count, wheel1, wheel2, wheel3) = match axis { let (count, wheel1, wheel2, wheel3) = match axis {
0 => (1, value, 0, 0), // 0 = vertical => 1 scroll wheel device (y axis) 0 => (1, value, 0, 0), // 0 = vertical => 1 scroll wheel device (y axis)
1 => (2, 0, value, 0), // 1 = horizontal => 2 scroll wheel devices (y, x) -> (0, x) 1 => (2, 0, value, 0), // 1 = horizontal => 2 scroll wheel devices (y, x) -> (0, x)
_ => { _ => {
log::warn!("invalid scroll event: {axis}, {value}"); log::warn!("invalid scroll event: {axis}, {value}");
return; return Ok(());
} }
}; };
let event = match CGEvent::new_scroll_event( let event = match CGEvent::new_scroll_event(
self.event_source.clone(), self.event_source.clone(),
ScrollEventUnit::LINE, ScrollEventUnit::PIXEL,
count, count,
wheel1, wheel1,
wheel2, wheel2,
@@ -240,7 +242,32 @@ impl InputEmulation for MacOSEmulation {
Ok(e) => e, Ok(e) => e,
Err(()) => { Err(()) => {
log::warn!("scroll event creation failed!"); log::warn!("scroll event creation failed!");
return; return Ok(());
}
};
event.post(CGEventTapLocation::HID);
}
PointerEvent::AxisDiscrete120 { axis, value } => {
let (count, wheel1, wheel2, wheel3) = match axis {
0 => (1, value, 0, 0), // 0 = vertical => 1 scroll wheel device (y axis)
1 => (2, 0, value, 0), // 1 = horizontal => 2 scroll wheel devices (y, x) -> (0, x)
_ => {
log::warn!("invalid scroll event: {axis}, {value}");
return Ok(());
}
};
let event = match CGEvent::new_scroll_event(
self.event_source.clone(),
ScrollEventUnit::PIXEL,
count,
wheel1,
wheel2,
wheel3,
) {
Ok(e) => e,
Err(()) => {
log::warn!("scroll event creation failed!");
return Ok(());
} }
}; };
event.post(CGEventTapLocation::HID); event.post(CGEventTapLocation::HID);
@@ -257,7 +284,7 @@ impl InputEmulation for MacOSEmulation {
Ok(k) => k.mac as CGKeyCode, Ok(k) => k.mac as CGKeyCode,
Err(_) => { Err(_) => {
log::warn!("unable to map key event"); log::warn!("unable to map key event");
return; return Ok(());
} }
}; };
match state { match state {
@@ -271,9 +298,11 @@ impl InputEmulation for MacOSEmulation {
}, },
_ => (), _ => (),
} }
// FIXME
Ok(())
} }
async fn notify(&mut self, _client_event: ClientEvent) {} async fn create(&mut self, _handle: EmulationHandle) {}
async fn destroy(&mut self) {} async fn destroy(&mut self, _handle: EmulationHandle) {}
} }

View File

@@ -1,25 +1,25 @@
use crate::{ use super::error::{EmulationError, WindowsEmulationCreationError};
emulate::InputEmulation, use input_event::{
event::{KeyboardEvent, PointerEvent}, scancode, Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE,
scancode, BTN_RIGHT,
}; };
use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use std::ops::BitOrAssign; use std::ops::BitOrAssign;
use std::time::Duration; use std::time::Duration;
use tokio::task::AbortHandle; use tokio::task::AbortHandle;
use windows::Win32::UI::Input::KeyboardAndMouse::{SendInput, INPUT_0, KEYEVENTF_EXTENDEDKEY}; use windows::Win32::UI::Input::KeyboardAndMouse::{
SendInput, INPUT_0, KEYEVENTF_EXTENDEDKEY, MOUSEEVENTF_XDOWN, MOUSEEVENTF_XUP,
};
use windows::Win32::UI::Input::KeyboardAndMouse::{ use windows::Win32::UI::Input::KeyboardAndMouse::{
INPUT, INPUT_KEYBOARD, INPUT_MOUSE, KEYBDINPUT, KEYEVENTF_KEYUP, KEYEVENTF_SCANCODE, INPUT, INPUT_KEYBOARD, INPUT_MOUSE, KEYBDINPUT, KEYEVENTF_KEYUP, KEYEVENTF_SCANCODE,
MOUSEEVENTF_HWHEEL, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_HWHEEL, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MIDDLEDOWN,
MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP, MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP,
MOUSEEVENTF_WHEEL, MOUSEINPUT, MOUSEEVENTF_WHEEL, MOUSEINPUT,
}; };
use windows::Win32::UI::WindowsAndMessaging::{XBUTTON1, XBUTTON2};
use crate::{ use super::{EmulationHandle, InputEmulation};
client::{ClientEvent, ClientHandle},
event::Event,
};
const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500); const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500);
const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32); const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32);
@@ -29,14 +29,14 @@ pub struct WindowsEmulation {
} }
impl WindowsEmulation { impl WindowsEmulation {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self, WindowsEmulationCreationError> {
Ok(Self { repeat_task: None }) Ok(Self { repeat_task: None })
} }
} }
#[async_trait] #[async_trait]
impl InputEmulation for WindowsEmulation { impl InputEmulation for WindowsEmulation {
async fn consume(&mut self, event: Event, _: ClientHandle) { async fn consume(&mut self, event: Event, _: EmulationHandle) -> Result<(), EmulationError> {
match event { match event {
Event::Pointer(pointer_event) => match pointer_event { Event::Pointer(pointer_event) => match pointer_event {
PointerEvent::Motion { PointerEvent::Motion {
@@ -55,7 +55,8 @@ impl InputEmulation for WindowsEmulation {
time: _, time: _,
axis, axis,
value, value,
} => scroll(axis, value), } => scroll(axis, value as i32),
PointerEvent::AxisDiscrete120 { axis, value } => scroll(axis, value),
PointerEvent::Frame {} => {} PointerEvent::Frame {} => {}
}, },
Event::Keyboard(keyboard_event) => match keyboard_event { Event::Keyboard(keyboard_event) => match keyboard_event {
@@ -76,13 +77,13 @@ impl InputEmulation for WindowsEmulation {
}, },
_ => {} _ => {}
} }
// FIXME
Ok(())
} }
async fn notify(&mut self, _: ClientEvent) { async fn create(&mut self, _handle: EmulationHandle) {}
// nothing to do
}
async fn destroy(&mut self) {} async fn destroy(&mut self, _handle: EmulationHandle) {}
} }
impl WindowsEmulation { impl WindowsEmulation {
@@ -145,23 +146,32 @@ fn rel_mouse(dx: i32, dy: i32) {
fn mouse_button(button: u32, state: u32) { fn mouse_button(button: u32, state: u32) {
let dw_flags = match state { let dw_flags = match state {
0 => match button { 0 => match button {
0x110 => MOUSEEVENTF_LEFTUP, BTN_LEFT => MOUSEEVENTF_LEFTUP,
0x111 => MOUSEEVENTF_RIGHTUP, BTN_RIGHT => MOUSEEVENTF_RIGHTUP,
0x112 => MOUSEEVENTF_MIDDLEUP, BTN_MIDDLE => MOUSEEVENTF_MIDDLEUP,
BTN_BACK => MOUSEEVENTF_XUP,
BTN_FORWARD => MOUSEEVENTF_XUP,
_ => return, _ => return,
}, },
1 => match button { 1 => match button {
0x110 => MOUSEEVENTF_LEFTDOWN, BTN_LEFT => MOUSEEVENTF_LEFTDOWN,
0x111 => MOUSEEVENTF_RIGHTDOWN, BTN_RIGHT => MOUSEEVENTF_RIGHTDOWN,
0x112 => MOUSEEVENTF_MIDDLEDOWN, BTN_MIDDLE => MOUSEEVENTF_MIDDLEDOWN,
BTN_BACK => MOUSEEVENTF_XDOWN,
BTN_FORWARD => MOUSEEVENTF_XDOWN,
_ => return, _ => return,
}, },
_ => return, _ => return,
}; };
let mouse_data = match button {
BTN_BACK => XBUTTON1 as u32,
BTN_FORWARD => XBUTTON2 as u32,
_ => 0,
};
let mi = MOUSEINPUT { let mi = MOUSEINPUT {
dx: 0, dx: 0,
dy: 0, // no movement dy: 0, // no movement
mouseData: 0, mouseData: mouse_data,
dwFlags: dw_flags, dwFlags: dw_flags,
time: 0, time: 0,
dwExtraInfo: 0, dwExtraInfo: 0,
@@ -169,7 +179,7 @@ fn mouse_button(button: u32, state: u32) {
send_mouse_input(mi); send_mouse_input(mi);
} }
fn scroll(axis: u8, value: f64) { fn scroll(axis: u8, value: i32) {
let event_type = match axis { let event_type = match axis {
0 => MOUSEEVENTF_WHEEL, 0 => MOUSEEVENTF_WHEEL,
1 => MOUSEEVENTF_HWHEEL, 1 => MOUSEEVENTF_HWHEEL,
@@ -178,7 +188,7 @@ fn scroll(axis: u8, value: f64) {
let mi = MOUSEINPUT { let mi = MOUSEINPUT {
dx: 0, dx: 0,
dy: 0, dy: 0,
mouseData: (-value * 15.0) as i32 as u32, mouseData: -value as u32,
dwFlags: event_type, dwFlags: event_type,
time: 0, time: 0,
dwExtraInfo: 0, dwExtraInfo: 0,

View File

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

View File

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

View File

@@ -0,0 +1,169 @@
use anyhow::Result;
use ashpd::{
desktop::{
remote_desktop::{Axis, DeviceType, KeyState, RemoteDesktop},
ResponseError, Session,
},
zbus::AsyncDrop,
WindowIdentifier,
};
use async_trait::async_trait;
use futures::FutureExt;
use input_event::{
Event::{Keyboard, Pointer},
KeyboardEvent, PointerEvent,
};
use crate::error::EmulationError;
use super::{error::XdpEmulationCreationError, EmulationHandle, InputEmulation};
pub struct DesktopPortalEmulation<'a> {
proxy: RemoteDesktop<'a>,
session: Session<'a>,
}
impl<'a> DesktopPortalEmulation<'a> {
pub async fn new() -> Result<DesktopPortalEmulation<'a>, XdpEmulationCreationError> {
log::debug!("connecting to org.freedesktop.portal.RemoteDesktop portal ...");
let proxy = RemoteDesktop::new().await?;
// retry when user presses the cancel button
let (session, _) = loop {
log::debug!("creating session ...");
let session = proxy.create_session().await?;
log::debug!("selecting devices ...");
proxy
.select_devices(&session, DeviceType::Keyboard | DeviceType::Pointer)
.await?;
log::info!("requesting permission for input emulation");
match proxy
.start(&session, &WindowIdentifier::default())
.await?
.response()
{
Ok(d) => break (session, d),
Err(ashpd::Error::Response(ResponseError::Cancelled)) => {
log::warn!("request cancelled!");
continue;
}
e => e?,
};
};
log::debug!("started session");
let session = session;
Ok(Self { proxy, session })
}
}
#[async_trait]
impl<'a> InputEmulation for DesktopPortalEmulation<'a> {
async fn consume(
&mut self,
event: input_event::Event,
_client: EmulationHandle,
) -> Result<(), EmulationError> {
match event {
Pointer(p) => match p {
PointerEvent::Motion {
time: _,
relative_x,
relative_y,
} => {
self.proxy
.notify_pointer_motion(&self.session, relative_x, relative_y)
.await?;
}
PointerEvent::Button {
time: _,
button,
state,
} => {
let state = match state {
0 => KeyState::Released,
_ => KeyState::Pressed,
};
self.proxy
.notify_pointer_button(&self.session, button as i32, state)
.await?;
}
PointerEvent::AxisDiscrete120 { axis, value } => {
let axis = match axis {
0 => Axis::Vertical,
_ => Axis::Horizontal,
};
self.proxy
.notify_pointer_axis_discrete(&self.session, axis, value)
.await?;
}
PointerEvent::Axis {
time: _,
axis,
value,
} => {
let axis = match axis {
0 => Axis::Vertical,
_ => Axis::Horizontal,
};
let (dx, dy) = match axis {
Axis::Vertical => (0., value),
Axis::Horizontal => (value, 0.),
};
self.proxy
.notify_pointer_axis(&self.session, dx, dy, true)
.await?;
}
PointerEvent::Frame {} => {}
},
Keyboard(k) => {
match k {
KeyboardEvent::Key {
time: _,
key,
state,
} => {
let state = match state {
0 => KeyState::Released,
_ => KeyState::Pressed,
};
self.proxy
.notify_keyboard_keycode(&self.session, key as i32, state)
.await?;
}
KeyboardEvent::Modifiers { .. } => {
// ignore
}
}
}
_ => {}
}
Ok(())
}
async fn create(&mut self, _client: EmulationHandle) {}
async fn destroy(&mut self, _client: EmulationHandle) {}
}
impl<'a> AsyncDrop for DesktopPortalEmulation<'a> {
#[doc = r" Perform the async cleanup."]
#[must_use]
#[allow(clippy::type_complexity, clippy::type_repetition_in_bounds)]
fn async_drop<'async_trait>(
self,
) -> ::core::pin::Pin<
Box<dyn ::core::future::Future<Output = ()> + ::core::marker::Send + 'async_trait>,
>
where
Self: 'async_trait,
{
async move {
let _ = self.session.close().await;
}
.boxed()
}
}

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

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

View File

@@ -4,6 +4,8 @@ use std::{
fmt::{self, Display}, fmt::{self, Display},
}; };
pub mod scancode;
// FIXME // FIXME
pub const BTN_LEFT: u32 = 0x110; pub const BTN_LEFT: u32 = 0x110;
pub const BTN_RIGHT: u32 = 0x111; pub const BTN_RIGHT: u32 = 0x111;
@@ -28,6 +30,10 @@ pub enum PointerEvent {
axis: u8, axis: u8,
value: f64, value: f64,
}, },
AxisDiscrete120 {
axis: u8,
value: i32,
},
Frame {}, Frame {},
} }
@@ -83,12 +89,29 @@ impl Display for PointerEvent {
time: _, time: _,
button, button,
state, state,
} => write!(f, "button({button}, {state})"), } => {
let str = match *button {
BTN_LEFT => Some("left"),
BTN_RIGHT => Some("right"),
BTN_MIDDLE => Some("middle"),
BTN_FORWARD => Some("forward"),
BTN_BACK => Some("back"),
_ => None,
};
if let Some(button) = str {
write!(f, "button({button}, {state})")
} else {
write!(f, "button({button}, {state}")
}
}
PointerEvent::Axis { PointerEvent::Axis {
time: _, time: _,
axis, axis,
value, value,
} => write!(f, "scroll({axis}, {value})"), } => write!(f, "scroll({axis}, {value})"),
PointerEvent::AxisDiscrete120 { axis, value } => {
write!(f, "scroll-120 ({axis}, {value})")
}
PointerEvent::Frame {} => write!(f, "frame()"), PointerEvent::Frame {} => write!(f, "frame()"),
} }
} }
@@ -101,7 +124,14 @@ impl Display for KeyboardEvent {
time: _, time: _,
key, key,
state, state,
} => write!(f, "key({key}, {state})"), } => {
let scan = scancode::Linux::try_from(*key);
if let Ok(scan) = scan {
write!(f, "key({scan:?}, {state})")
} else {
write!(f, "key({key}, {state})")
}
}
KeyboardEvent::Modifiers { KeyboardEvent::Modifiers {
mods_depressed, mods_depressed,
mods_latched, mods_latched,
@@ -149,6 +179,7 @@ impl PointerEvent {
Self::Motion { .. } => PointerEventType::Motion, Self::Motion { .. } => PointerEventType::Motion,
Self::Button { .. } => PointerEventType::Button, Self::Button { .. } => PointerEventType::Button,
Self::Axis { .. } => PointerEventType::Axis, Self::Axis { .. } => PointerEventType::Axis,
Self::AxisDiscrete120 { .. } => PointerEventType::AxisDiscrete120,
Self::Frame { .. } => PointerEventType::Frame, Self::Frame { .. } => PointerEventType::Frame,
} }
} }
@@ -167,6 +198,7 @@ enum PointerEventType {
Motion, Motion,
Button, Button,
Axis, Axis,
AxisDiscrete120,
Frame, Frame,
} }
enum KeyboardEventType { enum KeyboardEventType {
@@ -191,6 +223,7 @@ impl TryFrom<u8> for PointerEventType {
x if x == Self::Motion as u8 => Ok(Self::Motion), x if x == Self::Motion as u8 => Ok(Self::Motion),
x if x == Self::Button as u8 => Ok(Self::Button), x if x == Self::Button as u8 => Ok(Self::Button),
x if x == Self::Axis as u8 => Ok(Self::Axis), x if x == Self::Axis as u8 => Ok(Self::Axis),
x if x == Self::AxisDiscrete120 as u8 => Ok(Self::AxisDiscrete120),
x if x == Self::Frame as u8 => Ok(Self::Frame), x if x == Self::Frame as u8 => Ok(Self::Frame),
_ => Err(anyhow!(ProtocolError { _ => Err(anyhow!(ProtocolError {
msg: format!("invalid pointer event type {}", value), msg: format!("invalid pointer event type {}", value),
@@ -291,6 +324,11 @@ impl From<&PointerEvent> for Vec<u8> {
let value = value.to_be_bytes(); let value = value.to_be_bytes();
[&time[..], &axis[..], &value[..]].concat() [&time[..], &axis[..], &value[..]].concat()
} }
PointerEvent::AxisDiscrete120 { axis, value } => {
let axis = axis.to_be_bytes();
let value = value.to_be_bytes();
[&axis[..], &value[..]].concat()
}
PointerEvent::Frame {} => { PointerEvent::Frame {} => {
vec![] vec![]
} }
@@ -399,6 +437,25 @@ impl TryFrom<Vec<u8>> for PointerEvent {
}; };
Ok(Self::Axis { time, axis, value }) Ok(Self::Axis { time, axis, value })
} }
PointerEventType::AxisDiscrete120 => {
let axis = match data.get(2) {
Some(d) => *d,
None => {
return Err(anyhow!(ProtocolError {
msg: "Expected 1 Byte at index 2".into(),
}));
}
};
let value = match data.get(3..7) {
Some(d) => i32::from_be_bytes(d.try_into()?),
None => {
return Err(anyhow!(ProtocolError {
msg: "Expected 4 Bytes at index 3".into(),
}));
}
};
Ok(Self::AxisDiscrete120 { axis, value })
}
PointerEventType::Frame => Ok(Self::Frame {}), PointerEventType::Frame => Ok(Self::Frame {}),
} }
} }

View File

@@ -1,10 +1,13 @@
use num_enum::TryFromPrimitive;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/* /*
* https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input * https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input
* https://download.microsoft.com/download/1/6/1/161ba512-40e2-4cc9-843a-923143f3456c/translate.pdf
* https://kbd-project.org/docs/scancodes/scancodes-1.html
*/ */
#[repr(u32)] #[repr(u32)]
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy, TryFromPrimitive)]
pub enum Windows { pub enum Windows {
Shutdown = 0xE05E, Shutdown = 0xE05E,
SystemSleep = 0xE05F, SystemSleep = 0xE05F,
@@ -119,15 +122,15 @@ pub enum Windows {
KeyF21 = 0x006C, KeyF21 = 0x006C,
KeyF22 = 0x006D, KeyF22 = 0x006D,
KeyF23 = 0x006E, KeyF23 = 0x006E,
KeyF24 = 0x0076, KeyF24 = 0x0076, // KeyLANG5
KeypadComma = 0x007E, KeypadComma = 0x007E,
KeyInternational1 = 0x0073, KeyInternational1 = 0x0073,
KeyInternational2 = 0x0070, KeyInternational2 = 0x0070,
KeyInternational3 = 0x007D, KeyInternational3 = 0x007D, // typo in doc -> its Int'l 3 not Int'l 2
#[allow(dead_code)] #[allow(dead_code)]
KeyInternational4 = 0x0079, // FIXME unused KeyInternational4 = 0x0079,
#[allow(dead_code)] #[allow(dead_code)]
KeyInternational5 = 0x007B, // FIXME unused KeyInternational5 = 0x007B,
// KeyInternational6 = 0x005C, // KeyInternational6 = 0x005C,
KeyLANG1 = 0x0072, KeyLANG1 = 0x0072,
KeyLANG2 = 0x0071, KeyLANG2 = 0x0071,
@@ -140,6 +143,7 @@ pub enum Windows {
KeyLeftGUI = 0xE05B, KeyLeftGUI = 0xE05B,
KeyRightCtrl = 0xE01D, KeyRightCtrl = 0xE01D,
KeyRightShift = 0x0036, KeyRightShift = 0x0036,
KeyFakeRightShift = 0xE036,
KeyRightAlt = 0xE038, KeyRightAlt = 0xE038,
KeyRightGUI = 0xE05C, KeyRightGUI = 0xE05C,
KeyScanNextTrack = 0xE019, KeyScanNextTrack = 0xE019,
@@ -167,7 +171,7 @@ pub enum Windows {
* https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h * https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h
*/ */
#[repr(u32)] #[repr(u32)]
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, Hash, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, Hash, PartialEq, TryFromPrimitive)]
#[allow(dead_code)] #[allow(dead_code)]
pub enum Linux { pub enum Linux {
KeyReserved = 0, KeyReserved = 0,
@@ -292,7 +296,7 @@ pub enum Linux {
KeyPause = 119, KeyPause = 119,
KeyScale = 120, /* AL Compiz Scale (Expose) */ KeyScale = 120, /* AL Compiz Scale (Expose) */
KeyKpcomma = 121, KeyKpcomma = 121,
KeyHangeul = 122, KeyHanguel = 122,
// KEY_HANGUEL = KeyHangeul, // KEY_HANGUEL = KeyHangeul,
KeyHanja = 123, KeyHanja = 123,
KeyYen = 124, KeyYen = 124,
@@ -428,18 +432,6 @@ pub enum Linux {
KeyCount = 249, KeyCount = 249,
} }
impl TryFrom<u32> for Linux {
type Error = ();
fn try_from(value: u32) -> Result<Self, Self::Error> {
if value >= Self::KeyCount as u32 {
return Err(());
}
let code: Linux = unsafe { std::mem::transmute(value) };
Ok(code)
}
}
impl TryFrom<Linux> for Windows { impl TryFrom<Linux> for Windows {
type Error = (); type Error = ();
@@ -529,16 +521,16 @@ impl TryFrom<Linux> for Windows {
Linux::KeyKp3 => Ok(Self::Keypad3PageDn), Linux::KeyKp3 => Ok(Self::Keypad3PageDn),
Linux::KeyKp0 => Ok(Self::Keypad0Insert), Linux::KeyKp0 => Ok(Self::Keypad0Insert),
Linux::KeyKpDot => Ok(Self::KeypadDot), Linux::KeyKpDot => Ok(Self::KeypadDot),
Linux::KeyZenkakuhankaku => Ok(Self::KeyLANG1), // TODO unsure Linux::KeyZenkakuhankaku => Ok(Self::KeyF24), // KeyLANG5
Linux::Key102nd => Ok(Self::KeyNonUSSlashBar), // TODO unsure Linux::Key102nd => Ok(Self::KeyNonUSSlashBar), // TODO unsure
Linux::KeyF11 => Ok(Self::KeyF11), Linux::KeyF11 => Ok(Self::KeyF11),
Linux::KeyF12 => Ok(Self::KeyF12), Linux::KeyF12 => Ok(Self::KeyF12),
Linux::KeyRo => Ok(Self::ErrorRollOver), // TODO unsure Linux::KeyRo => Ok(Self::KeyInternational1),
Linux::KeyKatakana => Ok(Self::KeyLANG1), // TODO unsure Linux::KeyKatakana => Ok(Self::KeyLANG3),
Linux::KeyHiragana => Ok(Self::KeyLANG2), // TODO unsure Linux::KeyHiragana => Ok(Self::KeyLANG4),
Linux::KeyHenkan => Ok(Self::KeyLANG3), // TODO unsure Linux::KeyHenkan => Ok(Self::KeyInternational4),
Linux::KeyKatakanahiragana => Ok(Self::KeyLANG4), // TODO unsure Linux::KeyKatakanahiragana => Ok(Self::KeyInternational2),
Linux::KeyMuhenkan => Ok(Self::KeyLANG4), // TODO unsure Linux::KeyMuhenkan => Ok(Self::KeyInternational5),
Linux::KeyKpJpComma => Ok(Self::KeypadComma), Linux::KeyKpJpComma => Ok(Self::KeypadComma),
Linux::KeyKpEnter => Ok(Self::KeypadEnter), Linux::KeyKpEnter => Ok(Self::KeypadEnter),
Linux::KeyRightCtrl => Ok(Self::KeyRightCtrl), Linux::KeyRightCtrl => Ok(Self::KeyRightCtrl),
@@ -566,9 +558,9 @@ impl TryFrom<Linux> for Windows {
Linux::KeyPause => Ok(Self::KeyPause), Linux::KeyPause => Ok(Self::KeyPause),
Linux::KeyScale => Err(()), // TODO Linux::KeyScale => Err(()), // TODO
Linux::KeyKpcomma => Ok(Self::KeypadComma), Linux::KeyKpcomma => Ok(Self::KeypadComma),
Linux::KeyHangeul => Ok(Self::KeyInternational1), // TODO unsure Linux::KeyHanguel => Ok(Self::KeyLANG1), // FIXME should be 00F2?
Linux::KeyHanja => Ok(Self::KeyInternational2), // TODO unsure Linux::KeyHanja => Ok(Self::KeyLANG2), // FIXME should be 00F1?
Linux::KeyYen => Ok(Self::KeyInternational3), // TODO unsure Linux::KeyYen => Ok(Self::KeyInternational3),
Linux::KeyLeftMeta => Ok(Self::KeyLeftGUI), Linux::KeyLeftMeta => Ok(Self::KeyLeftGUI),
Linux::KeyRightmeta => Ok(Self::KeyRightGUI), Linux::KeyRightmeta => Ok(Self::KeyRightGUI),
Linux::KeyCompose => Ok(Self::KeyApplication), Linux::KeyCompose => Ok(Self::KeyApplication),
@@ -698,3 +690,162 @@ impl TryFrom<Linux> for Windows {
} }
} }
} }
impl TryFrom<Windows> for Linux {
type Error = ();
fn try_from(value: Windows) -> Result<Self, Self::Error> {
match value {
Windows::Shutdown => Ok(Self::KeyPower),
Windows::SystemSleep => Ok(Self::KeySleep),
Windows::SystemWakeUp => Ok(Self::KeyWakeup),
Windows::ErrorRollOver => Ok(Self::KeyRo),
Windows::KeyA => Ok(Self::KeyA),
Windows::KeyB => Ok(Self::KeyB),
Windows::KeyC => Ok(Self::KeyC),
Windows::KeyD => Ok(Self::KeyD),
Windows::KeyE => Ok(Self::KeyE),
Windows::KeyF => Ok(Self::KeyF),
Windows::KeyG => Ok(Self::KeyG),
Windows::KeyH => Ok(Self::KeyH),
Windows::KeyI => Ok(Self::KeyI),
Windows::KeyJ => Ok(Self::KeyJ),
Windows::KeyK => Ok(Self::KeyK),
Windows::KeyL => Ok(Self::KeyL),
Windows::KeyM => Ok(Self::KeyM),
Windows::KeyN => Ok(Self::KeyN),
Windows::KeyO => Ok(Self::KeyO),
Windows::KeyP => Ok(Self::KeyP),
Windows::KeyQ => Ok(Self::KeyQ),
Windows::KeyR => Ok(Self::KeyR),
Windows::KeyS => Ok(Self::KeyS),
Windows::KeyT => Ok(Self::KeyT),
Windows::KeyU => Ok(Self::KeyU),
Windows::KeyV => Ok(Self::KeyV),
Windows::KeyW => Ok(Self::KeyW),
Windows::KeyX => Ok(Self::KeyX),
Windows::KeyY => Ok(Self::KeyY),
Windows::KeyZ => Ok(Self::KeyZ),
Windows::Key1 => Ok(Self::Key1),
Windows::Key2 => Ok(Self::Key2),
Windows::Key3 => Ok(Self::Key3),
Windows::Key4 => Ok(Self::Key4),
Windows::Key5 => Ok(Self::Key5),
Windows::Key6 => Ok(Self::Key6),
Windows::Key7 => Ok(Self::Key7),
Windows::Key8 => Ok(Self::Key8),
Windows::Key9 => Ok(Self::Key9),
Windows::Key0 => Ok(Self::Key0),
Windows::KeyEnter => Ok(Self::KeyEnter),
Windows::KeyEsc => Ok(Self::KeyEsc),
Windows::KeyDelete => Ok(Self::KeyBackspace),
Windows::KeyTab => Ok(Self::KeyTab),
Windows::KeySpace => Ok(Self::KeySpace),
Windows::KeyMinus => Ok(Self::KeyMinus),
Windows::KeyEqual => Ok(Self::KeyEqual),
Windows::KeyLeftBrace => Ok(Self::KeyLeftbrace),
Windows::KeyRightBrace => Ok(Self::KeyRightbrace),
Windows::KeyBackslash => Ok(Self::KeyBackslash),
Windows::KeySemiColon => Ok(Self::KeySemicolon),
Windows::KeyApostrophe => Ok(Self::KeyApostrophe),
Windows::KeyGrave => Ok(Self::KeyGrave),
Windows::KeyComma => Ok(Self::KeyComma),
Windows::KeyDot => Ok(Self::KeyDot),
Windows::KeySlash => Ok(Self::KeySlash),
Windows::KeyCapsLock => Ok(Self::KeyCapsLock),
Windows::KeyF1 => Ok(Self::KeyF1),
Windows::KeyF2 => Ok(Self::KeyF2),
Windows::KeyF3 => Ok(Self::KeyF3),
Windows::KeyF4 => Ok(Self::KeyF4),
Windows::KeyF5 => Ok(Self::KeyF5),
Windows::KeyF6 => Ok(Self::KeyF6),
Windows::KeyF7 => Ok(Self::KeyF7),
Windows::KeyF8 => Ok(Self::KeyF8),
Windows::KeyF9 => Ok(Self::KeyF9),
Windows::KeyF10 => Ok(Self::KeyF10),
Windows::KeyF11 => Ok(Self::KeyF11),
Windows::KeyF12 => Ok(Self::KeyF12),
Windows::KeyPrintScreen => Ok(Self::KeySysrq),
Windows::KeyScrollLock => Ok(Self::KeyScrollLock),
Windows::KeyPause => Ok(Self::KeyPause),
Windows::KeyInsert => Ok(Self::KeyInsert),
Windows::KeyHome => Ok(Self::KeyHome),
Windows::KeyPageUp => Ok(Self::KeyPageup),
Windows::KeyDeleteForward => Ok(Self::KeyDelete),
Windows::KeyEnd => Ok(Self::KeyEnd),
Windows::KeyPageDown => Ok(Self::KeyPagedown),
Windows::KeyRight => Ok(Self::KeyRight),
Windows::KeyLeft => Ok(Self::KeyLeft),
Windows::KeyDown => Ok(Self::KeyDown),
Windows::KeyUp => Ok(Self::KeyUp),
Windows::KeypadNumLock => Ok(Self::KeyNumlock),
Windows::KeypadSlash => Ok(Self::KeyKpslash),
Windows::KeypadStar => Ok(Self::KeyKpAsterisk),
Windows::KeypadDash => Ok(Self::KeyKpMinus),
Windows::KeypadPlus => Ok(Self::KeyKpplus),
Windows::KeypadEnter => Ok(Self::KeyKpEnter),
Windows::Keypad1End => Ok(Self::KeyKp1),
Windows::Keypad2DownArrow => Ok(Self::KeyKp2),
Windows::Keypad3PageDn => Ok(Self::KeyKp3),
Windows::Keypad4LeftArrow => Ok(Self::KeyKp4),
Windows::Keypad5 => Ok(Self::KeyKp5),
Windows::Keypad6RightArrow => Ok(Self::KeyKp6),
Windows::Keypad7Home => Ok(Self::KeyKp7),
Windows::Keypad8UpArrow => Ok(Self::KeyKp8),
Windows::Keypad9PageUp => Ok(Self::KeyKp9),
Windows::Keypad0Insert => Ok(Self::KeyKp0),
Windows::KeypadDot => Ok(Self::KeyKpDot),
Windows::KeyNonUSSlashBar => Ok(Self::Key102nd),
Windows::KeyApplication => Ok(Self::KeyMenu),
Windows::KeypadEquals => Ok(Self::KeyKpequal),
Windows::KeyF13 => Ok(Self::KeyF13),
Windows::KeyF14 => Ok(Self::KeyF14),
Windows::KeyF15 => Ok(Self::KeyF15),
Windows::KeyF16 => Ok(Self::KeyF16),
Windows::KeyF17 => Ok(Self::KeyF17),
Windows::KeyF18 => Ok(Self::KeyF18),
Windows::KeyF19 => Ok(Self::KeyF19),
Windows::KeyF20 => Ok(Self::KeyF20),
Windows::KeyF21 => Ok(Self::KeyF21),
Windows::KeyF22 => Ok(Self::KeyF22),
Windows::KeyF23 => Ok(Self::KeyF23),
Windows::KeyF24 => Ok(Self::KeyF24),
Windows::KeypadComma => Ok(Self::KeyKpcomma),
Windows::KeyInternational1 => Ok(Self::KeyRo),
Windows::KeyInternational2 => Ok(Self::KeyKatakanahiragana),
Windows::KeyInternational3 => Ok(Self::KeyYen),
Windows::KeyInternational4 => Ok(Self::KeyHenkan),
Windows::KeyInternational5 => Ok(Self::KeyMuhenkan),
Windows::KeyLANG1 => Ok(Self::KeyHanguel),
Windows::KeyLANG2 => Ok(Self::KeyHanja),
Windows::KeyLANG3 => Ok(Self::KeyKatakana),
Windows::KeyLANG4 => Ok(Self::KeyHiragana),
Windows::KeyLeftCtrl => Ok(Self::KeyLeftCtrl),
Windows::KeyLeftShift => Ok(Self::KeyLeftShift),
Windows::KeyLeftAlt => Ok(Self::KeyLeftAlt),
Windows::KeyLeftGUI => Ok(Self::KeyLeftMeta),
Windows::KeyRightCtrl => Ok(Self::KeyRightCtrl),
Windows::KeyRightShift => Ok(Self::KeyRightShift),
Windows::KeyFakeRightShift => Ok(Self::KeyRightShift),
Windows::KeyRightAlt => Ok(Self::KeyRightalt),
Windows::KeyRightGUI => Ok(Self::KeyRightmeta),
Windows::KeyScanNextTrack => Ok(Self::KeyNextsong),
Windows::KeyScanPreviousTrack => Ok(Self::KeyPrevioussong),
Windows::KeyStop => Ok(Self::KeyStopcd),
Windows::KeyPlayPause => Ok(Self::KeyPlaypause),
Windows::KeyMute => Ok(Self::KeyMute),
Windows::KeyVolumeUp => Ok(Self::KeyVolumeUp),
Windows::KeyVolumeDown => Ok(Self::KeyVolumeDown),
Windows::ALConsumerControlConfiguration => Err(()),
Windows::ALEmailReader => Ok(Self::KeyMail),
Windows::ALCalculator => Ok(Self::KeyCalc),
Windows::ALLocalMachineBrowser => Ok(Self::KeyFile),
Windows::ACSearch => Ok(Self::KeyWww),
Windows::ACHome => Ok(Self::KeyHomepage),
Windows::ACBack => Ok(Self::KeyBack),
Windows::ACForward => Ok(Self::KeyForward),
Windows::ACStop => Ok(Self::KeyStop),
Windows::ACRefresh => Ok(Self::KeyRefresh),
Windows::ACBookmarks => Ok(Self::KeyBookmarks),
}
}
}

View File

@@ -34,6 +34,9 @@ enable lan-mouse
enable = true; enable = true;
# systemd = false; # systemd = false;
# package = inputs.lan-mouse.packages.${pkgs.stdenv.hostPlatform.system}.default # package = inputs.lan-mouse.packages.${pkgs.stdenv.hostPlatform.system}.default
# Optional configuration in nix syntax, see config.toml for available options
# settings = { };
};
}; };
} }

View File

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

View File

@@ -7,6 +7,7 @@ self: {
with lib; let with lib; let
cfg = config.programs.lan-mouse; cfg = config.programs.lan-mouse;
defaultPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.default; defaultPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.default;
tomlFormat = pkgs.formats.toml {};
in { in {
options.programs.lan-mouse = with types; { options.programs.lan-mouse = with types; {
enable = mkEnableOption "Whether or not to enable lan-mouse."; enable = mkEnableOption "Whether or not to enable lan-mouse.";
@@ -23,7 +24,23 @@ in {
systemd = mkOption { systemd = mkOption {
type = types.bool; type = types.bool;
default = pkgs.stdenv.isLinux; default = pkgs.stdenv.isLinux;
description = "Whether to enable to systemd service for lan-mouse."; description = "Whether to enable to systemd service for lan-mouse on linux.";
};
launchd = mkOption {
type = types.bool;
default = pkgs.stdenv.isDarwin;
description = "Whether to enable to launchd service for lan-mouse on macOS.";
};
settings = lib.mkOption {
inherit (tomlFormat) type;
default = {};
example = builtins.fromTOML (builtins.readFile (self + /config.toml));
description = ''
Optional configuration written to {file}`$XDG_CONFIG_HOME/lan-mouse/config.toml`.
See <https://github.com/feschber/lan-mouse/> for
available options and documentation.
'';
}; };
}; };
@@ -43,8 +60,23 @@ in {
]; ];
}; };
launchd.agents.lan-mouse = lib.mkIf cfg.launchd {
enable = true;
config = {
ProgramArguments = [
"${cfg.package}/bin/lan-mouse"
"--daemon"
];
KeepAlive = true;
};
};
home.packages = [ home.packages = [
cfg.package cfg.package
]; ];
xdg.configFile."lan-mouse/config.toml" = lib.mkIf (cfg.settings != {}) {
source = tomlFormat.generate "config.toml" cfg.settings;
};
}; };
} }

View File

@@ -11,6 +11,20 @@
<property name="tooltip-text" translatable="yes">enable</property> <property name="tooltip-text" translatable="yes">enable</property>
</object> </object>
</child> </child>
<child type="suffix">
<object class="GtkButton" id="dns_button">
<signal name="clicked" handler="handle_request_dns" swapped="true"/>
<!--<property name="icon-name">network-wired-disconnected-symbolic</property>-->
<property name="icon-name">network-wired-symbolic</property>
<property name="valign">center</property>
<property name="halign">end</property>
<property name="tooltip-text" translatable="yes">resolve host</property>
</object>
</child>
<child type="suffix">
<object class="GtkSpinner" id="dns_loading_indicator">
</object>
</child>
<!-- host --> <!-- host -->
<child> <child>
<object class="AdwActionRow"> <object class="AdwActionRow">
@@ -66,6 +80,7 @@
<property name="valign">center</property> <property name="valign">center</property>
<property name="halign">center</property> <property name="halign">center</property>
<property name="name">delete-button</property> <property name="name">delete-button</property>
<style><class name="error"/></style>
</object> </object>
</child> </child>
</object> </object>

View File

@@ -3,10 +3,8 @@
<gresource prefix="/de/feschber/LanMouse"> <gresource prefix="/de/feschber/LanMouse">
<file compressed="true" preprocess="xml-stripblanks">window.ui</file> <file compressed="true" preprocess="xml-stripblanks">window.ui</file>
<file compressed="true" preprocess="xml-stripblanks">client_row.ui</file> <file compressed="true" preprocess="xml-stripblanks">client_row.ui</file>
<file compressed="true">style.css</file>
<file compressed="true">style-dark.css</file>
</gresource> </gresource>
<gresource prefix="/de/feschber/LanMouse/icons"> <gresource prefix="/de/feschber/LanMouse/icons">
<file compressed="true" preprocess="xml-stripblanks">de.feschber.LanMouse.svg</file> <file compressed="true" preprocess="xml-stripblanks">de.feschber.LanMouse.svg</file>
</gresource> </gresource>
</gresources> </gresources>

View File

@@ -1,11 +0,0 @@
#delete-button {
color: @red_1;
}
#port-edit-cancel {
color: @red_1;
}
#port-edit-apply {
color: @green_1;
}

View File

@@ -1,11 +0,0 @@
#delete-button {
color: @red_3;
}
#port-edit-cancel {
color: @red_3;
}
#port-edit-apply {
color: @green_3;
}

View File

@@ -84,6 +84,7 @@
<property name="valign">center</property> <property name="valign">center</property>
<property name="visible">false</property> <property name="visible">false</property>
<property name="name">port-edit-apply</property> <property name="name">port-edit-apply</property>
<style><class name="success"/></style>
</object> </object>
</child> </child>
<child> <child>
@@ -93,6 +94,26 @@
<property name="valign">center</property> <property name="valign">center</property>
<property name="visible">false</property> <property name="visible">false</property>
<property name="name">port-edit-cancel</property> <property name="name">port-edit-cancel</property>
<style><class name="error"/></style>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title">hostname</property>
<child>
<object class="GtkLabel" id="hostname_label">
<property name="label">&lt;span font_style=&quot;italic&quot; font_weight=&quot;light&quot; foreground=&quot;darkgrey&quot;&gt;could not determine hostname&lt;/span&gt;</property>
<property name="use-markup">true</property>
<property name="valign">center</property>
</object>
</child>
<child>
<object class="GtkButton" id="copy-hostname-button">
<property name="icon-name">edit-copy-symbolic</property>
<property name="valign">center</property>
<signal name="clicked" handler="handle_copy_hostname" swapped="true"/>
</object> </object>
</child> </child>
</object> </object>

View File

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

View File

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

View File

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

View File

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

45
src/capture_test.rs Normal file
View File

@@ -0,0 +1,45 @@
use crate::config::Config;
use anyhow::{anyhow, Result};
use futures::StreamExt;
use input_capture::{self, Position};
use input_event::{Event, KeyboardEvent};
use tokio::task::LocalSet;
pub fn run() -> Result<()> {
log::info!("running input capture 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_capture_test(config)))
}
async fn input_capture_test(config: Config) -> Result<()> {
log::info!("creating input capture");
let backend = config.capture_backend.map(|b| b.into());
let mut input_capture = input_capture::create(backend).await?;
log::info!("creating clients");
input_capture.create(0, Position::Left)?;
input_capture.create(1, Position::Right)?;
input_capture.create(2, Position::Top)?;
input_capture.create(3, Position::Bottom)?;
loop {
let (client, event) = input_capture
.next()
.await
.ok_or(anyhow!("capture stream closed"))??;
let pos = match client {
0 => Position::Left,
1 => Position::Right,
2 => Position::Top,
_ => Position::Bottom,
};
log::info!("position: {pos}, event: {event}");
if let Event::Keyboard(KeyboardEvent::Key { key: 1, .. }) = event {
input_capture.release()?;
}
}
}

View File

@@ -1,10 +1,16 @@
use std::{ use std::{
collections::HashSet, collections::HashSet,
error::Error,
fmt::Display, fmt::Display,
net::{IpAddr, SocketAddr}, net::{IpAddr, SocketAddr},
str::FromStr,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use slab::Slab;
use crate::config::DEFAULT_PORT;
use input_capture;
#[derive(Debug, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)] #[derive(Debug, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)]
pub enum Position { pub enum Position {
@@ -20,13 +26,40 @@ impl Default for Position {
} }
} }
impl Position { impl From<Position> for input_capture::Position {
pub fn opposite(&self) -> Self { fn from(position: Position) -> input_capture::Position {
match self { match position {
Position::Left => Self::Right, Position::Left => input_capture::Position::Left,
Position::Right => Self::Left, Position::Right => input_capture::Position::Right,
Position::Top => Self::Bottom, Position::Top => input_capture::Position::Top,
Position::Bottom => Self::Top, Position::Bottom => input_capture::Position::Bottom,
}
}
}
#[derive(Debug)]
pub struct PositionParseError {
string: String,
}
impl Display for PositionParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "not a valid position: {}", self.string)
}
}
impl Error for PositionParseError {}
impl FromStr for Position {
type Err = PositionParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"left" => Ok(Self::Left),
"right" => Ok(Self::Right),
"top" => Ok(Self::Top),
"bottom" => Ok(Self::Bottom),
_ => Err(PositionParseError { string: s.into() }),
} }
} }
} }
@@ -61,37 +94,35 @@ impl TryFrom<&str> for Position {
} }
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub struct Client { pub struct ClientConfig {
/// hostname of this client /// hostname of this client
pub hostname: Option<String>, pub hostname: Option<String>,
/// fix ips, determined by the user /// fix ips, determined by the user
pub fix_ips: Vec<IpAddr>, pub fix_ips: Vec<IpAddr>,
/// unique handle to refer to the client.
/// This way any emulation / capture backend does not
/// need to know anything about a client other than its handle.
pub handle: ClientHandle,
/// 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>,
/// both active_addr and addrs can be None / empty so port needs to be stored seperately /// both active_addr and addrs can be None / empty so port needs to be stored seperately
pub port: u16, pub port: u16,
/// position of a client on screen /// position of a client on screen
pub pos: Position, pub pos: Position,
/// enter hook
pub cmd: Option<String>,
} }
#[derive(Clone, Copy, Debug)] impl Default for ClientConfig {
pub enum ClientEvent { fn default() -> Self {
Create(ClientHandle, Position), Self {
Destroy(ClientHandle), port: DEFAULT_PORT,
hostname: Default::default(),
fix_ips: Default::default(),
pos: Default::default(),
cmd: None,
}
}
} }
pub type ClientHandle = u32; pub type ClientHandle = u64;
#[derive(Debug, Clone)] #[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct ClientState { pub struct ClientState {
/// information about the client
pub client: Client,
/// events should be sent to and received from the client /// events should be sent to and received from the client
pub active: bool, pub active: bool,
/// `active` address of the client, used to send data to. /// `active` address of the client, used to send data to.
@@ -100,12 +131,18 @@ pub struct ClientState {
pub active_addr: Option<SocketAddr>, pub active_addr: Option<SocketAddr>,
/// tracks whether or not the client is responding to pings /// tracks whether or not the client is responding to pings
pub alive: bool, pub alive: bool,
/// all ip addresses associated with a particular client
/// e.g. Laptops usually have at least an ethernet and a wifi port
/// which have different ip addresses
pub ips: HashSet<IpAddr>,
/// keys currently pressed by this client /// keys currently pressed by this client
pub pressed_keys: HashSet<u32>, pub pressed_keys: HashSet<u32>,
/// dns resolving in progress
pub resolving: bool,
} }
pub struct ClientManager { pub struct ClientManager {
clients: Vec<Option<ClientState>>, // HashMap likely not beneficial clients: Slab<(ClientConfig, ClientState)>,
} }
impl Default for ClientManager { impl Default for ClientManager {
@@ -116,50 +153,15 @@ impl Default for ClientManager {
impl ClientManager { impl ClientManager {
pub fn new() -> Self { pub fn new() -> Self {
Self { clients: vec![] } let clients = Slab::new();
Self { clients }
} }
/// add a new client to this manager /// add a new client to this manager
pub fn add_client( pub fn add_client(&mut self) -> ClientHandle {
&mut self, let client_config = Default::default();
hostname: Option<String>, let client_state = Default::default();
ips: HashSet<IpAddr>, self.clients.insert((client_config, client_state)) as ClientHandle
port: u16,
pos: Position,
active: bool,
) -> ClientHandle {
// get a new client_handle
let handle = self.free_id();
// store fix ip addresses
let fix_ips = ips.iter().cloned().collect();
// store the client
let client = Client {
hostname,
fix_ips,
handle,
ips,
port,
pos,
};
// client was never seen, nor pinged
let client_state = ClientState {
client,
active,
active_addr: None,
alive: false,
pressed_keys: HashSet::new(),
};
if handle as usize >= self.clients.len() {
assert_eq!(handle as usize, self.clients.len());
self.clients.push(Some(client_state));
} else {
self.clients[handle as usize] = Some(client_state);
}
handle
} }
/// find a client by its address /// find a client by its address
@@ -168,49 +170,54 @@ impl ClientManager {
// time this is likely faster than using a HashMap // time this is likely faster than using a HashMap
self.clients self.clients
.iter() .iter()
.position(|c| { .find_map(|(k, (_, s))| {
if let Some(c) = c { if s.active && s.ips.contains(&addr.ip()) {
c.active && c.client.ips.contains(&addr.ip()) Some(k)
} else { } else {
false None
}
})
.map(|p| p as ClientHandle)
}
pub fn find_client(&self, pos: Position) -> Option<ClientHandle> {
self.clients
.iter()
.find_map(|(k, (c, s))| {
if s.active && c.pos == pos {
Some(k)
} else {
None
} }
}) })
.map(|p| p as ClientHandle) .map(|p| p as ClientHandle)
} }
/// remove a client from the list /// remove a client from the list
pub fn remove_client(&mut self, client: ClientHandle) -> Option<ClientState> { pub fn remove_client(&mut self, client: ClientHandle) -> Option<(ClientConfig, ClientState)> {
// remove id from occupied ids // remove id from occupied ids
self.clients.get_mut(client as usize)?.take() self.clients.try_remove(client as usize)
}
/// get a free slot in the client list
fn free_id(&mut self) -> ClientHandle {
for i in 0..u32::MAX {
if self.clients.get(i as usize).is_none()
|| self.clients.get(i as usize).unwrap().is_none()
{
return i;
}
}
panic!("Out of client ids");
} }
// returns an immutable reference to the client state corresponding to `client` // returns an immutable reference to the client state corresponding to `client`
pub fn get(&self, client: ClientHandle) -> Option<&ClientState> { pub fn get(&self, handle: ClientHandle) -> Option<&(ClientConfig, ClientState)> {
self.clients.get(client as usize)?.as_ref() self.clients.get(handle as usize)
} }
/// returns a mutable reference to the client state corresponding to `client` /// returns a mutable reference to the client state corresponding to `client`
pub fn get_mut(&mut self, client: ClientHandle) -> Option<&mut ClientState> { pub fn get_mut(&mut self, handle: ClientHandle) -> Option<&mut (ClientConfig, ClientState)> {
self.clients.get_mut(client as usize)?.as_mut() self.clients.get_mut(handle as usize)
} }
pub fn get_client_states(&self) -> impl Iterator<Item = &ClientState> { pub fn get_client_states(
self.clients.iter().filter_map(|x| x.as_ref()) &self,
) -> impl Iterator<Item = (ClientHandle, &(ClientConfig, ClientState))> {
self.clients.iter().map(|(k, v)| (k as ClientHandle, v))
} }
pub fn get_client_states_mut(&mut self) -> impl Iterator<Item = &mut ClientState> { pub fn get_client_states_mut(
self.clients.iter_mut().filter_map(|x| x.as_mut()) &mut self,
) -> impl Iterator<Item = (ClientHandle, &mut (ClientConfig, ClientState))> {
self.clients.iter_mut().map(|(k, v)| (k as ClientHandle, v))
} }
} }

View File

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

View File

@@ -1,7 +1,7 @@
use anyhow::Result; use anyhow::Result;
use std::{error::Error, net::IpAddr}; use std::{error::Error, net::IpAddr};
use trust_dns_resolver::TokioAsyncResolver; use hickory_resolver::TokioAsyncResolver;
pub struct DnsResolver { pub struct DnsResolver {
resolver: TokioAsyncResolver, resolver: TokioAsyncResolver,

View File

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

View File

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

View File

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

View File

@@ -1,154 +0,0 @@
use anyhow::Result;
use ashpd::{
desktop::{
remote_desktop::{Axis, DeviceType, KeyState, RemoteDesktop},
ResponseError, Session,
},
WindowIdentifier,
};
use async_trait::async_trait;
use crate::{
client::ClientEvent,
emulate::InputEmulation,
event::{
Event::{Keyboard, Pointer},
KeyboardEvent, PointerEvent,
},
};
pub struct DesktopPortalEmulation<'a> {
proxy: RemoteDesktop<'a>,
session: Session<'a>,
}
impl<'a> DesktopPortalEmulation<'a> {
pub async fn new() -> Result<DesktopPortalEmulation<'a>> {
log::debug!("connecting to org.freedesktop.portal.RemoteDesktop portal ...");
let proxy = RemoteDesktop::new().await?;
// retry when user presses the cancel button
let (session, _) = loop {
log::debug!("creating session ...");
let session = proxy.create_session().await?;
log::debug!("selecting devices ...");
proxy
.select_devices(&session, DeviceType::Keyboard | DeviceType::Pointer)
.await?;
log::info!("requesting permission for input emulation");
match proxy
.start(&session, &WindowIdentifier::default())
.await?
.response()
{
Ok(d) => break (session, d),
Err(ashpd::Error::Response(ResponseError::Cancelled)) => {
log::warn!("request cancelled!");
continue;
}
e => e?,
};
};
log::debug!("started session");
Ok(Self { proxy, session })
}
}
#[async_trait]
impl<'a> InputEmulation for DesktopPortalEmulation<'a> {
async fn consume(&mut self, event: crate::event::Event, _client: crate::client::ClientHandle) {
match event {
Pointer(p) => {
match p {
PointerEvent::Motion {
time: _,
relative_x,
relative_y,
} => {
if let Err(e) = self
.proxy
.notify_pointer_motion(&self.session, relative_x, relative_y)
.await
{
log::warn!("{e}");
}
}
PointerEvent::Button {
time: _,
button,
state,
} => {
let state = match state {
0 => KeyState::Released,
_ => KeyState::Pressed,
};
if let Err(e) = self
.proxy
.notify_pointer_button(&self.session, button as i32, state)
.await
{
log::warn!("{e}");
}
}
PointerEvent::Axis {
time: _,
axis,
value,
} => {
let axis = match axis {
0 => Axis::Vertical,
_ => Axis::Horizontal,
};
// TODO smooth scrolling
if let Err(e) = self
.proxy
.notify_pointer_axis_discrete(&self.session, axis, value as i32)
.await
{
log::warn!("{e}");
}
}
PointerEvent::Frame {} => {}
}
}
Keyboard(k) => {
match k {
KeyboardEvent::Key {
time: _,
key,
state,
} => {
let state = match state {
0 => KeyState::Released,
_ => KeyState::Pressed,
};
if let Err(e) = self
.proxy
.notify_keyboard_keycode(&self.session, key as i32, state)
.await
{
log::warn!("{e}");
}
}
KeyboardEvent::Modifiers { .. } => {
// ignore
}
}
}
_ => {}
}
}
async fn notify(&mut self, _client: ClientEvent) {}
async fn destroy(&mut self) {
log::debug!("closing remote desktop session");
if let Err(e) = self.session.close().await {
log::error!("failed to close remote desktop session: {e}");
}
}
}

49
src/emulation_test.rs Normal file
View File

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

View File

@@ -1,5 +1,5 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use std::{cmp::min, io::ErrorKind, str, time::Duration}; use std::{cmp::min, io::ErrorKind, net::IpAddr, str, time::Duration};
#[cfg(unix)] #[cfg(unix)]
use std::{ use std::{
@@ -23,7 +23,7 @@ use tokio::net::TcpStream;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
client::{Client, ClientHandle, Position}, client::{ClientConfig, ClientHandle, ClientState, Position},
config::{Config, Frontend}, config::{Config, Frontend},
}; };
@@ -84,34 +84,49 @@ pub fn wait_for_service() -> Result<std::net::TcpStream> {
} }
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub enum FrontendEvent { pub enum FrontendRequest {
/// add a new client
AddClient(Option<String>, u16, Position),
/// activate/deactivate client /// activate/deactivate client
ActivateClient(ClientHandle, bool), Activate(ClientHandle, bool),
/// add a new client
Create,
/// change the listen port (recreate udp listener) /// change the listen port (recreate udp listener)
ChangePort(u16), ChangePort(u16),
/// remove a client /// remove a client
DelClient(ClientHandle), Delete(ClientHandle),
/// request an enumertaion of all clients /// request an enumeration of all clients
Enumerate(), Enumerate(),
/// resolve dns
ResolveDns(ClientHandle),
/// service shutdown /// service shutdown
Shutdown(), Terminate(),
/// update a client (hostname, port, position) /// update hostname
UpdateClient(ClientHandle, Option<String>, u16, Position), UpdateHostname(ClientHandle, Option<String>),
/// update port
UpdatePort(ClientHandle, u16),
/// update position
UpdatePosition(ClientHandle, Position),
/// update fix-ips
UpdateFixIps(ClientHandle, Vec<IpAddr>),
/// request the state of the given client
GetState(ClientHandle),
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FrontendNotify { pub enum FrontendEvent {
NotifyClientActivate(ClientHandle, bool), /// a client was created
NotifyClientCreate(Client), Created(ClientHandle, ClientConfig, ClientState),
NotifyClientUpdate(Client), /// no such client
NotifyClientDelete(ClientHandle), NoSuchClient(ClientHandle),
/// state changed
State(ClientHandle, ClientConfig, ClientState),
/// the client was deleted
Deleted(ClientHandle),
/// new port, reason of failure (if failed) /// new port, reason of failure (if failed)
NotifyPortChange(u16, Option<String>), PortChanged(u16, Option<String>),
/// Client State, active /// list of all clients, used for initial state synchronization
Enumerate(Vec<(Client, bool)>), Enumerate(Vec<(ClientHandle, ClientConfig, ClientState)>),
NotifyError(String), /// an error occured
Error(String),
} }
pub struct FrontendListener { pub struct FrontendListener {
@@ -217,13 +232,12 @@ impl FrontendListener {
Ok(rx) Ok(rx)
} }
pub(crate) async fn notify_all(&mut self, notify: FrontendNotify) -> Result<()> { pub(crate) async fn broadcast_event(&mut self, notify: FrontendEvent) -> Result<()> {
// encode event // encode event
let json = serde_json::to_string(&notify).unwrap(); let json = serde_json::to_string(&notify).unwrap();
let payload = json.as_bytes(); let payload = json.as_bytes();
let len = payload.len().to_be_bytes(); let len = payload.len().to_be_bytes();
log::debug!("json: {json}, len: {}", payload.len()); log::debug!("broadcasting event to streams: {json}");
let mut keep = vec![]; let mut keep = vec![];
// TODO do simultaneously // TODO do simultaneously
for tx in self.tx_streams.iter_mut() { for tx in self.tx_streams.iter_mut() {
@@ -255,7 +269,7 @@ impl Drop for FrontendListener {
} }
#[cfg(unix)] #[cfg(unix)]
pub async fn read_event(stream: &mut ReadHalf<UnixStream>) -> Result<FrontendEvent> { pub async fn wait_for_request(stream: &mut ReadHalf<UnixStream>) -> Result<FrontendRequest> {
let len = stream.read_u64().await?; let len = stream.read_u64().await?;
assert!(len <= 256); assert!(len <= 256);
let mut buf = [0u8; 256]; let mut buf = [0u8; 256];
@@ -264,7 +278,7 @@ pub async fn read_event(stream: &mut ReadHalf<UnixStream>) -> Result<FrontendEve
} }
#[cfg(windows)] #[cfg(windows)]
pub async fn read_event(stream: &mut ReadHalf<TcpStream>) -> Result<FrontendEvent> { pub async fn wait_for_request(stream: &mut ReadHalf<TcpStream>) -> Result<FrontendRequest> {
let len = stream.read_u64().await?; let len = stream.read_u64().await?;
let mut buf = [0u8; 256]; let mut buf = [0u8; 256];
stream.read_exact(&mut buf[..len as usize]).await?; stream.read_exact(&mut buf[..len as usize]).await?;

View File

@@ -1,242 +1,325 @@
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Result};
use tokio::{
use std::{ io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader},
io::{ErrorKind, Read, Write}, task::LocalSet,
str::SplitWhitespace,
thread,
}; };
use crate::{client::Position, config::DEFAULT_PORT}; #[cfg(windows)]
use tokio::net::tcp::{ReadHalf, WriteHalf};
#[cfg(unix)]
use tokio::net::unix::{ReadHalf, WriteHalf};
use super::{FrontendEvent, FrontendNotify}; use std::io::{self, Write};
use crate::{
client::{ClientConfig, ClientHandle, ClientState},
config::DEFAULT_PORT,
};
use self::command::{Command, CommandType};
use super::{FrontendEvent, FrontendRequest};
mod command;
pub fn run() -> Result<()> { pub fn run() -> Result<()> {
let Ok(mut tx) = super::wait_for_service() else { let Ok(stream) = super::wait_for_service() else {
return Err(anyhow!("Could not connect to lan-mouse-socket")); return Err(anyhow!("Could not connect to lan-mouse-socket"));
}; };
let mut rx = tx.try_clone()?; let runtime = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()?;
runtime.block_on(LocalSet::new().run_until(async move {
stream.set_nonblocking(true)?;
#[cfg(unix)]
let mut stream = tokio::net::UnixStream::from_std(stream)?;
#[cfg(windows)]
let mut stream = tokio::net::TcpStream::from_std(stream)?;
let (rx, tx) = stream.split();
let reader = thread::Builder::new() let mut cli = Cli::new(rx, tx);
.name("cli-frontend".to_string()) cli.run().await
.spawn(move || { }))?;
// all further prompts
prompt();
loop {
let mut buf = String::new();
match std::io::stdin().read_line(&mut buf) {
Ok(0) => return,
Ok(len) => {
if let Some(events) = parse_cmd(buf, len) {
for event in events.iter() {
let json = serde_json::to_string(&event).unwrap();
let bytes = json.as_bytes();
let len = bytes.len().to_be_bytes();
if let Err(e) = tx.write(&len) {
log::error!("error sending message: {e}");
};
if let Err(e) = tx.write(bytes) {
log::error!("error sending message: {e}");
};
if *event == FrontendEvent::Shutdown() {
return;
}
}
// prompt is printed after the server response is received
} else {
prompt();
}
}
Err(e) => {
if e.kind() != ErrorKind::UnexpectedEof {
log::error!("error reading from stdin: {e}");
}
return;
}
}
}
})?;
let _ = thread::Builder::new()
.name("cli-frontend-notify".to_string())
.spawn(move || {
loop {
// read len
let mut len = [0u8; 8];
match rx.read_exact(&mut len) {
Ok(()) => (),
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break,
Err(e) => break log::error!("{e}"),
};
let len = usize::from_be_bytes(len);
// read payload
let mut buf: Vec<u8> = vec![0u8; len];
match rx.read_exact(&mut buf[..len]) {
Ok(()) => (),
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break,
Err(e) => break log::error!("{e}"),
};
let notify: FrontendNotify = match serde_json::from_slice(&buf) {
Ok(n) => n,
Err(e) => break log::error!("{e}"),
};
match notify {
FrontendNotify::NotifyClientActivate(handle, active) => {
if active {
log::info!("client {handle} activated");
} else {
log::info!("client {handle} deactivated");
}
}
FrontendNotify::NotifyClientCreate(client) => {
let handle = client.handle;
let port = client.port;
let pos = client.pos;
let hostname = client.hostname.as_deref().unwrap_or("");
log::info!("new client ({handle}): {hostname}:{port} - {pos}");
}
FrontendNotify::NotifyClientUpdate(client) => {
let handle = client.handle;
let port = client.port;
let pos = client.pos;
let hostname = client.hostname.as_deref().unwrap_or("");
log::info!("client ({handle}) updated: {hostname}:{port} - {pos}");
}
FrontendNotify::NotifyClientDelete(client) => {
log::info!("client ({client}) deleted.");
}
FrontendNotify::NotifyError(e) => {
log::warn!("{e}");
}
FrontendNotify::Enumerate(clients) => {
for (client, active) in clients.into_iter() {
log::info!(
"client ({}) [{}]: active: {}, associated addresses: [{}]",
client.handle,
client.hostname.as_deref().unwrap_or(""),
if active { "yes" } else { "no" },
client
.ips
.into_iter()
.map(|a| a.to_string())
.collect::<Vec<String>>()
.join(", ")
);
}
}
FrontendNotify::NotifyPortChange(port, msg) => match msg {
Some(msg) => log::info!("could not change port: {msg}"),
None => log::info!("port changed: {port}"),
},
}
prompt();
}
})?;
match reader.join() {
Ok(_) => {}
Err(e) => {
let msg = match (e.downcast_ref::<&str>(), e.downcast_ref::<String>()) {
(Some(&s), _) => s,
(_, Some(s)) => s,
_ => "no panic info",
};
log::error!("reader thread paniced: {msg}");
}
}
Ok(()) Ok(())
} }
fn prompt() { struct Cli<'a> {
clients: Vec<(ClientHandle, ClientConfig, ClientState)>,
rx: ReadHalf<'a>,
tx: WriteHalf<'a>,
}
impl<'a> Cli<'a> {
fn new(rx: ReadHalf<'a>, tx: WriteHalf<'a>) -> Cli<'a> {
Self {
clients: vec![],
rx,
tx,
}
}
async fn run(&mut self) -> Result<()> {
let stdin = tokio::io::stdin();
let stdin = BufReader::new(stdin);
let mut stdin = stdin.lines();
/* initial state sync */
let request = FrontendRequest::Enumerate();
self.send_request(request).await?;
self.clients = loop {
let event = self.await_event().await?;
if let FrontendEvent::Enumerate(clients) = event {
break clients;
}
};
loop {
prompt()?;
tokio::select! {
line = stdin.next_line() => {
let Some(line) = line? else {
break Ok(());
};
let cmd: Command = match line.parse() {
Ok(cmd) => cmd,
Err(e) => {
eprintln!("{e}");
continue;
}
};
self.execute(cmd).await?;
}
event = self.await_event() => {
let event = event?;
self.handle_event(event);
}
}
}
}
async fn update_client(&mut self, handle: ClientHandle) -> Result<()> {
self.send_request(FrontendRequest::GetState(handle)).await?;
loop {
let event = self.await_event().await?;
self.handle_event(event.clone());
if let FrontendEvent::State(_, _, _) | FrontendEvent::NoSuchClient(_) = event {
break;
}
}
Ok(())
}
async fn execute(&mut self, cmd: Command) -> Result<()> {
match cmd {
Command::None => {}
Command::Connect(pos, host, port) => {
let request = FrontendRequest::Create;
self.send_request(request).await?;
let handle = loop {
let event = self.await_event().await?;
match event {
FrontendEvent::Created(h, c, s) => {
self.clients.push((h, c, s));
break h;
}
_ => {
self.handle_event(event);
continue;
}
}
};
for request in [
FrontendRequest::UpdateHostname(handle, Some(host.clone())),
FrontendRequest::UpdatePort(handle, port.unwrap_or(DEFAULT_PORT)),
FrontendRequest::UpdatePosition(handle, pos),
] {
self.send_request(request).await?;
}
self.update_client(handle).await?;
}
Command::Disconnect(id) => {
self.send_request(FrontendRequest::Delete(id)).await?;
loop {
let event = self.await_event().await?;
self.handle_event(event.clone());
if let FrontendEvent::Deleted(_) = event {
self.handle_event(event);
break;
}
}
}
Command::Activate(id) => {
self.send_request(FrontendRequest::Activate(id, true))
.await?;
self.update_client(id).await?;
}
Command::Deactivate(id) => {
self.send_request(FrontendRequest::Activate(id, false))
.await?;
self.update_client(id).await?;
}
Command::List => {
self.send_request(FrontendRequest::Enumerate()).await?;
loop {
let event = self.await_event().await?;
self.handle_event(event.clone());
if let FrontendEvent::Enumerate(_) = event {
break;
}
}
}
Command::SetHost(handle, host) => {
let request = FrontendRequest::UpdateHostname(handle, Some(host.clone()));
self.send_request(request).await?;
self.update_client(handle).await?;
}
Command::SetPort(handle, port) => {
let request = FrontendRequest::UpdatePort(handle, port.unwrap_or(DEFAULT_PORT));
self.send_request(request).await?;
self.update_client(handle).await?;
}
Command::Help => {
for cmd_type in [
CommandType::List,
CommandType::Connect,
CommandType::Disconnect,
CommandType::Activate,
CommandType::Deactivate,
CommandType::SetHost,
CommandType::SetPort,
] {
eprintln!("{}", cmd_type.usage());
}
}
}
Ok(())
}
fn find_mut(
&mut self,
handle: ClientHandle,
) -> Option<&mut (ClientHandle, ClientConfig, ClientState)> {
self.clients.iter_mut().find(|(h, _, _)| *h == handle)
}
fn remove(
&mut self,
handle: ClientHandle,
) -> Option<(ClientHandle, ClientConfig, ClientState)> {
let idx = self.clients.iter().position(|(h, _, _)| *h == handle);
idx.map(|i| self.clients.swap_remove(i))
}
fn handle_event(&mut self, event: FrontendEvent) {
match event {
FrontendEvent::Created(h, c, s) => {
eprint!("client added ({h}): ");
print_config(&c);
eprint!(" ");
print_state(&s);
eprintln!();
self.clients.push((h, c, s));
}
FrontendEvent::NoSuchClient(h) => {
eprintln!("no such client: {h}");
}
FrontendEvent::State(h, c, s) => {
if let Some((_, config, state)) = self.find_mut(h) {
let old_host = config.hostname.clone().unwrap_or("\"\"".into());
let new_host = c.hostname.clone().unwrap_or("\"\"".into());
if old_host != new_host {
eprintln!(
"client {h}: hostname updated ({} -> {})",
old_host, new_host
);
}
if config.port != c.port {
eprintln!("client {h} changed port: {} -> {}", config.port, c.port);
}
if config.fix_ips != c.fix_ips {
eprintln!("client {h} ips updated: {:?}", c.fix_ips)
}
*config = c;
if state.active ^ s.active {
eprintln!(
"client {h} {}",
if s.active { "activated" } else { "deactivated" }
);
}
*state = s;
}
}
FrontendEvent::Deleted(h) => {
if let Some((h, c, _)) = self.remove(h) {
eprint!("client {h} removed (");
print_config(&c);
eprintln!(")");
}
}
FrontendEvent::PortChanged(p, e) => {
if let Some(e) = e {
eprintln!("failed to change port: {e}");
} else {
eprintln!("changed port to {p}");
}
}
FrontendEvent::Enumerate(clients) => {
self.clients = clients;
self.print_clients();
}
FrontendEvent::Error(e) => {
eprintln!("ERROR: {e}");
}
}
}
fn print_clients(&mut self) {
for (h, c, s) in self.clients.iter() {
eprint!("client {h}: ");
print_config(c);
eprint!(" ");
print_state(s);
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<()> {
eprint!("lan-mouse > "); eprint!("lan-mouse > ");
std::io::stderr().flush().unwrap(); std::io::stderr().flush()?;
Ok(())
} }
fn parse_cmd(s: String, len: usize) -> Option<Vec<FrontendEvent>> { fn print_config(c: &ClientConfig) {
if len == 0 { eprint!(
return Some(vec![FrontendEvent::Shutdown()]); "{}:{} ({}), ips: {:?}",
} c.hostname.clone().unwrap_or("(no hostname)".into()),
let mut l = s.split_whitespace(); c.port,
let cmd = l.next()?; c.pos,
let res = match cmd { c.fix_ips
"help" => { );
log::info!("list list clients");
log::info!("connect <host> left|right|top|bottom [port] add a new client");
log::info!("disconnect <client> remove a client");
log::info!("activate <client> activate a client");
log::info!("deactivate <client> deactivate a client");
log::info!("exit exit lan-mouse");
log::info!("setport <port> change port");
None
}
"exit" => return Some(vec![FrontendEvent::Shutdown()]),
"list" => return Some(vec![FrontendEvent::Enumerate()]),
"connect" => Some(parse_connect(l)),
"disconnect" => Some(parse_disconnect(l)),
"activate" => Some(parse_activate(l)),
"deactivate" => Some(parse_deactivate(l)),
"setport" => Some(parse_port(l)),
_ => {
log::error!("unknown command: {s}");
None
}
};
match res {
Some(Ok(e)) => Some(e),
Some(Err(e)) => {
log::warn!("{e}");
None
}
_ => None,
}
} }
fn parse_connect(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> { fn print_state(s: &ClientState) {
let usage = "usage: connect <host> left|right|top|bottom [port]"; eprint!("active: {}, dns: {:?}", s.active, s.ips);
let host = l.next().context(usage)?.to_owned();
let pos = match l.next().context(usage)? {
"right" => Position::Right,
"top" => Position::Top,
"bottom" => Position::Bottom,
_ => Position::Left,
};
let port = if let Some(p) = l.next() {
p.parse()?
} else {
DEFAULT_PORT
};
Ok(vec![
FrontendEvent::AddClient(Some(host), port, pos),
FrontendEvent::Enumerate(),
])
}
fn parse_disconnect(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> {
let client = l.next().context("usage: disconnect <client_id>")?.parse()?;
Ok(vec![
FrontendEvent::DelClient(client),
FrontendEvent::Enumerate(),
])
}
fn parse_activate(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> {
let client = l.next().context("usage: activate <client_id>")?.parse()?;
Ok(vec![
FrontendEvent::ActivateClient(client, true),
FrontendEvent::Enumerate(),
])
}
fn parse_deactivate(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> {
let client = l.next().context("usage: deactivate <client_id>")?.parse()?;
Ok(vec![
FrontendEvent::ActivateClient(client, false),
FrontendEvent::Enumerate(),
])
}
fn parse_port(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> {
let port = l.next().context("usage: setport <port>")?.parse()?;
Ok(vec![FrontendEvent::ChangePort(port)])
} }

153
src/frontend/cli/command.rs Normal file
View File

@@ -0,0 +1,153 @@
use std::{
fmt::Display,
str::{FromStr, SplitWhitespace},
};
use crate::client::{ClientHandle, Position};
pub(super) enum CommandType {
NoCommand,
Help,
Connect,
Disconnect,
Activate,
Deactivate,
List,
SetHost,
SetPort,
}
#[derive(Debug)]
pub(super) struct InvalidCommand {
cmd: String,
}
impl Display for InvalidCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "invalid command: \"{}\"", self.cmd)
}
}
impl FromStr for CommandType {
type Err = InvalidCommand;
fn from_str(s: &str) -> std::prelude::v1::Result<Self, Self::Err> {
match s {
"connect" => Ok(Self::Connect),
"disconnect" => Ok(Self::Disconnect),
"activate" => Ok(Self::Activate),
"deactivate" => Ok(Self::Deactivate),
"list" => Ok(Self::List),
"set-host" => Ok(Self::SetHost),
"set-port" => Ok(Self::SetPort),
"help" => Ok(Self::Help),
_ => Err(InvalidCommand { cmd: s.to_string() }),
}
}
}
#[derive(Debug)]
pub(super) enum Command {
None,
Help,
Connect(Position, String, Option<u16>),
Disconnect(ClientHandle),
Activate(ClientHandle),
Deactivate(ClientHandle),
List,
SetHost(ClientHandle, String),
SetPort(ClientHandle, Option<u16>),
}
impl CommandType {
pub(super) fn usage(&self) -> &'static str {
match self {
CommandType::Help => "help",
CommandType::NoCommand => "",
CommandType::Connect => "connect left|right|top|bottom <host> [<port>]",
CommandType::Disconnect => "disconnect <id>",
CommandType::Activate => "activate <id>",
CommandType::Deactivate => "deactivate <id>",
CommandType::List => "list",
CommandType::SetHost => "set-host <id> <host>",
CommandType::SetPort => "set-port <id> <host>",
}
}
}
pub(super) enum CommandParseError {
Usage(CommandType),
Invalid(InvalidCommand),
}
impl Display for CommandParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Usage(cmd) => write!(f, "usage: {}", cmd.usage()),
Self::Invalid(cmd) => write!(f, "{}", cmd),
}
}
}
impl FromStr for Command {
type Err = CommandParseError;
fn from_str(cmd: &str) -> Result<Self, Self::Err> {
let mut args = cmd.split_whitespace();
let cmd_type: CommandType = match args.next() {
Some(c) => c.parse().map_err(CommandParseError::Invalid),
None => Ok(CommandType::NoCommand),
}?;
match cmd_type {
CommandType::Help => Ok(Command::Help),
CommandType::NoCommand => Ok(Command::None),
CommandType::Connect => parse_connect_cmd(args),
CommandType::Disconnect => parse_disconnect_cmd(args),
CommandType::Activate => parse_activate_cmd(args),
CommandType::Deactivate => parse_deactivate_cmd(args),
CommandType::List => Ok(Command::List),
CommandType::SetHost => parse_set_host(args),
CommandType::SetPort => parse_set_port(args),
}
}
}
fn parse_connect_cmd(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::Connect);
let pos = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
let host = args.next().ok_or(USAGE)?.to_string();
let port = args.next().and_then(|p| p.parse().ok());
Ok(Command::Connect(pos, host, port))
}
fn parse_disconnect_cmd(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::Disconnect);
let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
Ok(Command::Disconnect(id))
}
fn parse_activate_cmd(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::Activate);
let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
Ok(Command::Activate(id))
}
fn parse_deactivate_cmd(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::Deactivate);
let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
Ok(Command::Deactivate(id))
}
fn parse_set_host(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::SetHost);
let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
let host = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
Ok(Command::SetHost(id, host))
}
fn parse_set_port(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::SetPort);
let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
let port = args.next().and_then(|p| p.parse().ok());
Ok(Command::SetPort(id, port))
}

View File

@@ -8,18 +8,18 @@ use std::{
process, str, process, str,
}; };
use crate::frontend::gtk::window::Window; use crate::frontend::{gtk::window::Window, FrontendRequest};
use adw::Application; use adw::Application;
use endi::{Endian, ReadBytes};
use gtk::{ use gtk::{
gdk::Display, glib::clone, prelude::*, subclass::prelude::ObjectSubclassIsExt, CssProvider, gdk::Display, glib::clone, prelude::*, subclass::prelude::ObjectSubclassIsExt, IconTheme,
IconTheme,
}; };
use gtk::{gio, glib, prelude::ApplicationExt}; use gtk::{gio, glib, prelude::ApplicationExt};
use self::client_object::ClientObject; use self::client_object::ClientObject;
use super::FrontendNotify; use super::FrontendEvent;
pub fn run() -> glib::ExitCode { pub fn run() -> glib::ExitCode {
log::debug!("running gtk frontend"); log::debug!("running gtk frontend");
@@ -51,23 +51,12 @@ fn gtk_main() -> glib::ExitCode {
.build(); .build();
app.connect_startup(|_| load_icons()); app.connect_startup(|_| load_icons());
app.connect_startup(|_| load_css());
app.connect_activate(build_ui); app.connect_activate(build_ui);
let args: Vec<&'static str> = vec![]; let args: Vec<&'static str> = vec![];
app.run_with_args(&args) app.run_with_args(&args)
} }
fn load_css() {
let provider = CssProvider::new();
provider.load_from_resource("de/feschber/LanMouse/style.css");
gtk::style_context_add_provider_for_display(
&Display::default().expect("Could not connect to a display."),
&provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}
fn load_icons() { fn load_icons() {
let display = &Display::default().expect("Could not connect to a display."); let display = &Display::default().expect("Could not connect to a display.");
let icon_theme = IconTheme::for_display(display); let icon_theme = IconTheme::for_display(display);
@@ -97,16 +86,14 @@ fn build_ui(app: &Application) {
gio::spawn_blocking(move || { gio::spawn_blocking(move || {
match loop { match loop {
// read length // read length
let mut len = [0u8; 8]; let len = match rx.read_u64(Endian::Big) {
match rx.read_exact(&mut len) { Ok(l) => l,
Ok(_) => (),
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()), Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()),
Err(e) => break Err(e), Err(e) => break Err(e),
}; };
let len = usize::from_be_bytes(len);
// read payload // read payload
let mut buf = vec![0u8; len]; let mut buf = vec![0u8; len as usize];
match rx.read_exact(&mut buf) { match rx.read_exact(&mut buf) {
Ok(_) => (), Ok(_) => (),
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()), Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()),
@@ -125,38 +112,38 @@ fn build_ui(app: &Application) {
} }
}); });
let window = Window::new(app); let window = Window::new(app, tx);
window.imp().stream.borrow_mut().replace(tx); window.request(FrontendRequest::Enumerate());
glib::spawn_future_local(clone!(@weak window => async move { glib::spawn_future_local(clone!(@weak window => async move {
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 {
FrontendNotify::NotifyClientActivate(handle, active) => { FrontendEvent::Created(handle, client, state) => {
window.activate_client(handle, active); window.new_client(handle, client, state);
}
FrontendNotify::NotifyClientCreate(client) => {
window.new_client(client, false);
}, },
FrontendNotify::NotifyClientUpdate(client) => { FrontendEvent::Deleted(client) => {
window.update_client(client);
}
FrontendNotify::NotifyError(e) => {
window.show_toast(e.as_str());
},
FrontendNotify::NotifyClientDelete(client) => {
window.delete_client(client); window.delete_client(client);
} }
FrontendNotify::Enumerate(clients) => { FrontendEvent::State(handle, config, state) => {
for (client, active) in clients { window.update_client_config(handle, config);
if window.client_idx(client.handle).is_some() { window.update_client_state(handle, state);
window.activate_client(client.handle, active); }
window.update_client(client); FrontendEvent::NoSuchClient(_) => { }
FrontendEvent::Error(e) => {
window.show_toast(e.as_str());
},
FrontendEvent::Enumerate(clients) => {
for (handle, client, state) in clients {
if window.client_idx(handle).is_some() {
window.update_client_config(handle, client);
window.update_client_state(handle, state);
} else { } else {
window.new_client(client, active); window.new_client(handle, client, state);
} }
} }
}, },
FrontendNotify::NotifyPortChange(port, msg) => { FrontendEvent::PortChanged(port, msg) => {
match msg { match msg {
None => window.show_toast(format!("port changed: {port}").as_str()), None => window.show_toast(format!("port changed: {port}").as_str()),
Some(msg) => window.show_toast(msg.as_str()), Some(msg) => window.show_toast(msg.as_str()),

View File

@@ -3,20 +3,29 @@ mod imp;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use gtk::glib::{self, Object}; use gtk::glib::{self, Object};
use crate::client::{Client, ClientHandle}; use crate::client::{ClientConfig, ClientHandle, ClientState};
glib::wrapper! { glib::wrapper! {
pub struct ClientObject(ObjectSubclass<imp::ClientObject>); pub struct ClientObject(ObjectSubclass<imp::ClientObject>);
} }
impl ClientObject { impl ClientObject {
pub fn new(client: Client, active: bool) -> Self { pub fn new(handle: ClientHandle, client: ClientConfig, state: ClientState) -> Self {
Object::builder() Object::builder()
.property("handle", client.handle) .property("handle", handle)
.property("hostname", client.hostname) .property("hostname", client.hostname)
.property("port", client.port as u32) .property("port", client.port as u32)
.property("position", client.pos.to_string()) .property("position", client.pos.to_string())
.property("active", active) .property("active", state.active)
.property(
"ips",
state
.ips
.iter()
.map(|ip| ip.to_string())
.collect::<Vec<_>>(),
)
.property("resolving", state.resolving)
.build() .build()
} }
@@ -32,4 +41,6 @@ pub struct ClientData {
pub port: u32, pub port: u32,
pub active: bool, pub active: bool,
pub position: String, pub position: String,
pub resolving: bool,
pub ips: Vec<String>,
} }

View File

@@ -17,6 +17,8 @@ pub struct ClientObject {
#[property(name = "port", get, set, type = u32, member = port, maximum = u16::MAX as u32)] #[property(name = "port", get, set, type = u32, member = port, maximum = u16::MAX as u32)]
#[property(name = "active", get, set, type = bool, member = active)] #[property(name = "active", get, set, type = bool, member = active)]
#[property(name = "position", get, set, type = String, member = position)] #[property(name = "position", get, set, type = String, member = position)]
#[property(name = "resolving", get, set, type = bool, member = resolving)]
#[property(name = "ips", get, set, type = Vec<String>, member = ips)]
pub data: RefCell<ClientData>, pub data: RefCell<ClientData>,
} }

View File

@@ -109,6 +109,27 @@ impl ClientRow {
.sync_create() .sync_create()
.build(); .build();
let resolve_binding = client_object
.bind_property(
"resolving",
&self.imp().dns_loading_indicator.get(),
"spinning",
)
.sync_create()
.build();
let ip_binding = client_object
.bind_property("ips", &self.imp().dns_button.get(), "tooltip-text")
.transform_to(|_, ips: Vec<String>| {
if ips.is_empty() {
Some("no ip addresses associated with this client".into())
} else {
Some(ips.join("\n"))
}
})
.sync_create()
.build();
bindings.push(active_binding); bindings.push(active_binding);
bindings.push(switch_position_binding); bindings.push(switch_position_binding);
bindings.push(hostname_binding); bindings.push(hostname_binding);
@@ -116,6 +137,8 @@ impl ClientRow {
bindings.push(port_binding); bindings.push(port_binding);
bindings.push(subtitle_binding); bindings.push(subtitle_binding);
bindings.push(position_binding); bindings.push(position_binding);
bindings.push(resolve_binding);
bindings.push(ip_binding);
} }
pub fn unbind(&self) { pub fn unbind(&self) {

View File

@@ -14,6 +14,8 @@ pub struct ClientRow {
#[template_child] #[template_child]
pub enable_switch: TemplateChild<gtk::Switch>, pub enable_switch: TemplateChild<gtk::Switch>,
#[template_child] #[template_child]
pub dns_button: TemplateChild<gtk::Button>,
#[template_child]
pub hostname: TemplateChild<gtk::Entry>, pub hostname: TemplateChild<gtk::Entry>,
#[template_child] #[template_child]
pub port: TemplateChild<gtk::Entry>, pub port: TemplateChild<gtk::Entry>,
@@ -23,6 +25,8 @@ pub struct ClientRow {
pub delete_row: TemplateChild<ActionRow>, pub delete_row: TemplateChild<ActionRow>,
#[template_child] #[template_child]
pub delete_button: TemplateChild<gtk::Button>, pub delete_button: TemplateChild<gtk::Button>,
#[template_child]
pub dns_loading_indicator: TemplateChild<gtk::Spinner>,
pub bindings: RefCell<Vec<Binding>>, pub bindings: RefCell<Vec<Binding>>,
} }
@@ -58,6 +62,7 @@ impl ObjectImpl for ClientRow {
static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new(); static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
SIGNALS.get_or_init(|| { SIGNALS.get_or_init(|| {
vec![ vec![
Signal::builder("request-dns").build(),
Signal::builder("request-update") Signal::builder("request-update")
.param_types([bool::static_type()]) .param_types([bool::static_type()])
.build(), .build(),
@@ -76,6 +81,11 @@ impl ClientRow {
true // dont run default handler true // dont run default handler
} }
#[template_callback]
fn handle_request_dns(&self, _: Button) {
self.obj().emit_by_name::<()>("request-dns", &[]);
}
#[template_callback] #[template_callback]
fn handle_client_delete(&self, _button: &Button) { fn handle_client_delete(&self, _button: &Button) {
log::debug!("delete button pressed -> requesting delete"); log::debug!("delete button pressed -> requesting delete");

View File

@@ -2,19 +2,26 @@ mod imp;
use std::io::Write; 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,
glib::{self, closure_local}, glib::{self, closure_local},
NoSelection, ListBox, NoSelection,
}; };
use crate::{ use crate::{
client::{Client, ClientHandle, Position}, client::{ClientConfig, ClientHandle, ClientState, Position},
config::DEFAULT_PORT, config::DEFAULT_PORT,
frontend::{gtk::client_object::ClientObject, FrontendEvent}, frontend::{gtk::client_object::ClientObject, FrontendRequest},
}; };
use super::client_row::ClientRow; use super::client_row::ClientRow;
@@ -27,8 +34,14 @@ glib::wrapper! {
} }
impl Window { impl Window {
pub(crate) fn new(app: &adw::Application) -> Self { pub(crate) fn new(
Object::builder().property("application", app).build() app: &adw::Application,
#[cfg(unix)] tx: UnixStream,
#[cfg(windows)] tx: TcpStream,
) -> Self {
let window: Self = Object::builder().property("application", app).build();
window.imp().stream.borrow_mut().replace(tx);
window
} }
pub fn clients(&self) -> gio::ListStore { pub fn clients(&self) -> gio::ListStore {
@@ -39,6 +52,10 @@ impl Window {
.expect("Could not get clients") .expect("Could not get clients")
} }
fn client_by_idx(&self, idx: u32) -> Option<ClientObject> {
self.clients().item(idx).map(|o| o.downcast().unwrap())
}
fn setup_clients(&self) { fn setup_clients(&self) {
let model = gio::ListStore::new::<ClientObject>(); let model = gio::ListStore::new::<ClientObject>();
self.imp().clients.replace(Some(model)); self.imp().clients.replace(Some(model));
@@ -50,16 +67,24 @@ impl Window {
let client_object = obj.downcast_ref().expect("Expected object of type `ClientObject`."); let client_object = obj.downcast_ref().expect("Expected object of type `ClientObject`.");
let row = window.create_client_row(client_object); let row = window.create_client_row(client_object);
row.connect_closure("request-update", false, closure_local!(@strong window => move |row: ClientRow, active: bool| { row.connect_closure("request-update", false, closure_local!(@strong window => move |row: ClientRow, active: bool| {
let index = row.index() as u32; if let Some(client) = window.client_by_idx(row.index() as u32) {
let Some(client) = window.clients().item(index) else { window.request_client_activate(&client, active);
return; window.request_client_update(&client);
}; window.request_client_state(&client);
let client = client.downcast_ref::<ClientObject>().unwrap(); }
window.request_client_update(client, active);
})); }));
row.connect_closure("request-delete", false, closure_local!(@strong window => move |row: ClientRow| { row.connect_closure("request-delete", false, closure_local!(@strong window => move |row: ClientRow| {
let index = row.index() as u32; if let Some(client) = window.client_by_idx(row.index() as u32) {
window.request_client_delete(index); window.request_client_delete(&client);
}
}));
row.connect_closure("request-dns", false, closure_local!(@strong window => move
|row: ClientRow| {
if let Some(client) = window.client_by_idx(row.index() as u32) {
window.request_client_update(&client);
window.request_dns(&client);
window.request_client_state(&client);
}
})); }));
row.upcast() row.upcast()
}) })
@@ -87,10 +112,11 @@ impl Window {
row row
} }
pub fn new_client(&self, client: Client, active: bool) { pub fn new_client(&self, handle: ClientHandle, client: ClientConfig, state: ClientState) {
let client = ClientObject::new(client, active); let client = ClientObject::new(handle, client, state.clone());
self.clients().append(&client); self.clients().append(&client);
self.set_placeholder_visible(false); self.set_placeholder_visible(false);
self.update_dns_state(handle, !state.ips.is_empty());
} }
pub fn client_idx(&self, handle: ClientHandle) -> Option<usize> { pub fn client_idx(&self, handle: ClientHandle) -> Option<usize> {
@@ -115,9 +141,9 @@ impl Window {
} }
} }
pub fn update_client(&self, client: Client) { pub fn update_client_config(&self, handle: ClientHandle, client: ClientConfig) {
let Some(idx) = self.client_idx(client.handle) else { let Some(idx) = self.client_idx(handle) else {
log::warn!("could not find client with handle {}", client.handle); log::warn!("could not find client with handle {}", handle);
return; return;
}; };
let client_object = self.clients().item(idx as u32).unwrap(); let client_object = self.clients().item(idx as u32).unwrap();
@@ -137,75 +163,110 @@ impl Window {
} }
} }
pub fn activate_client(&self, handle: ClientHandle, active: bool) { pub fn update_client_state(&self, handle: ClientHandle, state: ClientState) {
let Some(idx) = self.client_idx(handle) else { let Some(idx) = self.client_idx(handle) else {
log::warn!("could not find client with handle {handle}"); log::warn!("could not find client with handle {}", handle);
return; return;
}; };
let client_object = self.clients().item(idx as u32).unwrap(); let client_object = self.clients().item(idx as u32).unwrap();
let client_object: &ClientObject = client_object.downcast_ref().unwrap(); let client_object: &ClientObject = client_object.downcast_ref().unwrap();
let data = client_object.get_data(); let data = client_object.get_data();
if data.active != active {
client_object.set_active(active); if state.active != data.active {
log::debug!("set active to {active}"); client_object.set_active(state.active);
log::debug!("set active to {}", state.active);
} }
if state.resolving != data.resolving {
client_object.set_resolving(state.resolving);
log::debug!("resolving {}: {}", data.handle, state.resolving);
}
self.update_dns_state(handle, !state.ips.is_empty());
let ips = state
.ips
.into_iter()
.map(|ip| ip.to_string())
.collect::<Vec<_>>();
client_object.set_ips(ips);
} }
pub fn request_client_create(&self) { pub fn update_dns_state(&self, handle: ClientHandle, resolved: bool) {
let event = FrontendEvent::AddClient(None, DEFAULT_PORT, Position::default()); let Some(idx) = self.client_idx(handle) else {
self.imp().set_port(DEFAULT_PORT); log::warn!("could not find client with handle {}", handle);
self.request(event); return;
};
let list_box: ListBox = self.imp().client_list.get();
let row = list_box.row_at_index(idx as i32).unwrap();
let client_row: ClientRow = row.downcast().expect("expected ClientRow Object");
if resolved {
client_row.imp().dns_button.set_css_classes(&["success"])
} else {
client_row.imp().dns_button.set_css_classes(&["warning"])
}
} }
pub fn request_port_change(&self) { pub fn request_port_change(&self) {
let port = self.imp().port_entry.get().text().to_string(); let port = self.imp().port_entry.get().text().to_string();
if let Ok(port) = port.as_str().parse::<u16>() { if let Ok(port) = port.as_str().parse::<u16>() {
self.request(FrontendEvent::ChangePort(port)); self.request(FrontendRequest::ChangePort(port));
} else { } else {
self.request(FrontendEvent::ChangePort(DEFAULT_PORT)); self.request(FrontendRequest::ChangePort(DEFAULT_PORT));
} }
} }
pub fn request_client_update(&self, client: &ClientObject, active: bool) { pub fn request_client_state(&self, client: &ClientObject) {
let data = client.get_data(); let handle = client.handle();
let position = match Position::try_from(data.position.as_str()) { let event = FrontendRequest::GetState(handle);
Ok(pos) => pos,
_ => {
log::error!("invalid position: {}", data.position);
return;
}
};
let hostname = data.hostname;
let port = data.port as u16;
let event = FrontendEvent::UpdateClient(client.handle(), hostname, port, position);
log::debug!("requesting update: {event:?}");
self.request(event);
let event = FrontendEvent::ActivateClient(client.handle(), active);
log::debug!("requesting activate: {event:?}");
self.request(event); self.request(event);
} }
pub fn request_client_delete(&self, idx: u32) { pub fn request_client_create(&self) {
if let Some(obj) = self.clients().item(idx) { let event = FrontendRequest::Create;
let client_object: &ClientObject = obj self.request(event);
.downcast_ref() }
.expect("Expected object of type `ClientObject`.");
let handle = client_object.handle(); pub fn request_dns(&self, client: &ClientObject) {
let event = FrontendEvent::DelClient(handle); let data = client.get_data();
let event = FrontendRequest::ResolveDns(data.handle);
self.request(event);
}
pub fn request_client_update(&self, client: &ClientObject) {
let handle = client.handle();
let data = client.get_data();
let position = Position::try_from(data.position.as_str()).expect("invalid position");
let hostname = data.hostname;
let port = data.port as u16;
for event in [
FrontendRequest::UpdateHostname(handle, hostname),
FrontendRequest::UpdatePosition(handle, position),
FrontendRequest::UpdatePort(handle, port),
] {
self.request(event); self.request(event);
} }
} }
fn request(&self, event: FrontendEvent) { pub fn request_client_activate(&self, client: &ClientObject, active: bool) {
let handle = client.handle();
let event = FrontendRequest::Activate(handle, active);
self.request(event);
}
pub fn request_client_delete(&self, client: &ClientObject) {
let handle = client.handle();
let event = FrontendRequest::Delete(handle);
self.request(event);
}
pub fn request(&self, event: FrontendRequest) {
let json = serde_json::to_string(&event).unwrap(); let json = serde_json::to_string(&event).unwrap();
log::debug!("requesting {json}"); log::debug!("requesting: {json}");
let mut stream = self.imp().stream.borrow_mut(); let mut stream = self.imp().stream.borrow_mut();
let stream = stream.as_mut().unwrap(); let stream = stream.as_mut().unwrap();
let bytes = json.as_bytes(); let bytes = json.as_bytes();
let len = bytes.len().to_be_bytes(); if let Err(e) = stream.write_u64(Endian::Big, bytes.len() as u64) {
if let Err(e) = stream.write(&len) {
log::error!("error sending message: {e}"); log::error!("error sending message: {e}");
}; };
if let Err(e) = stream.write(bytes) { if let Err(e) = stream.write(bytes) {

View File

@@ -8,7 +8,8 @@ use std::os::unix::net::UnixStream;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use adw::{prelude::*, ActionRow, ToastOverlay}; use adw::{prelude::*, ActionRow, ToastOverlay};
use glib::subclass::InitializingObject; use glib::subclass::InitializingObject;
use gtk::{gio, glib, Button, CompositeTemplate, Entry, ListBox}; use gtk::glib::clone;
use gtk::{gdk, gio, glib, Button, CompositeTemplate, Entry, Label, ListBox};
use crate::config::DEFAULT_PORT; use crate::config::DEFAULT_PORT;
@@ -26,6 +27,8 @@ pub struct Window {
#[template_child] #[template_child]
pub port_entry: TemplateChild<Entry>, pub port_entry: TemplateChild<Entry>,
#[template_child] #[template_child]
pub hostname_label: TemplateChild<Label>,
#[template_child]
pub toast_overlay: TemplateChild<ToastOverlay>, pub toast_overlay: TemplateChild<ToastOverlay>,
pub clients: RefCell<Option<gio::ListStore>>, pub clients: RefCell<Option<gio::ListStore>>,
#[cfg(unix)] #[cfg(unix)]
@@ -61,6 +64,22 @@ impl Window {
self.obj().request_client_create(); self.obj().request_client_create();
} }
#[template_callback]
fn handle_copy_hostname(&self, button: &Button) {
if let Ok(hostname) = hostname::get() {
let display = gdk::Display::default().unwrap();
let clipboard = display.clipboard();
clipboard.set_text(hostname.to_str().expect("hostname: invalid utf8"));
button.set_icon_name("emblem-ok-symbolic");
button.set_css_classes(&["success"]);
glib::spawn_future_local(clone!(@weak button => async move {
glib::timeout_future_seconds(1).await;
button.set_icon_name("edit-copy-symbolic");
button.set_css_classes(&[]);
}));
}
}
#[template_callback] #[template_callback]
fn handle_port_changed(&self, _entry: &Entry) { fn handle_port_changed(&self, _entry: &Entry) {
self.port_edit_apply.set_visible(true); self.port_edit_apply.set_visible(true);
@@ -95,6 +114,10 @@ impl Window {
impl ObjectImpl for Window { impl ObjectImpl for Window {
fn constructed(&self) { fn constructed(&self) {
if let Ok(hostname) = hostname::get() {
self.hostname_label
.set_text(hostname.to_str().expect("hostname: invalid utf8"));
}
self.parent_constructed(); self.parent_constructed();
self.set_port(DEFAULT_PORT); self.set_port(DEFAULT_PORT);
let obj = self.obj(); let obj = self.obj();

View File

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

View File

@@ -2,7 +2,7 @@ use anyhow::Result;
use std::process::{self, Child, Command}; use std::process::{self, Child, Command};
use env_logger::Env; use env_logger::Env;
use lan_mouse::{config::Config, frontend, server::Server}; use lan_mouse::{capture_test, config::Config, emulation_test, frontend, server::Server};
use tokio::task::LocalSet; use tokio::task::LocalSet;
@@ -31,7 +31,11 @@ pub fn run() -> Result<()> {
log::debug!("{config:?}"); log::debug!("{config:?}");
log::info!("release bind: {:?}", config.release_bind); log::info!("release bind: {:?}", config.release_bind);
if config.daemon { if config.test_capture {
capture_test::run()?;
} else if config.test_emulation {
emulation_test::run()?;
} else if config.daemon {
// if daemon is specified we run the service // if daemon is specified we run the service
run_service(&config)?; run_service(&config)?;
} else { } else {
@@ -64,10 +68,12 @@ fn run_service(config: &Config) -> Result<()> {
// run async event loop // run async event loop
runtime.block_on(LocalSet::new().run_until(async { runtime.block_on(LocalSet::new().run_until(async {
// run main loop // run main loop
log::info!("Press Ctrl+Alt+Shift+Super to release the mouse"); log::info!("Press {:?} to release the mouse", config.release_bind);
let server = Server::new(config); let server = Server::new(config);
server.run().await?; server
.run(config.capture_backend, config.emulation_backend)
.await?;
log::debug!("service exiting"); log::debug!("service exiting");
anyhow::Ok(()) anyhow::Ok(())

View File

@@ -1,16 +1,16 @@
use log; use log;
use std::{ use std::{
cell::{Cell, RefCell}, cell::{Cell, RefCell},
collections::HashSet,
rc::Rc, rc::Rc,
}; };
use tokio::signal; use tokio::signal;
use crate::{capture, emulate};
use crate::{ use crate::{
client::{ClientHandle, ClientManager}, client::{ClientConfig, ClientHandle, ClientManager, ClientState},
config::Config, config::{CaptureBackend, Config, EmulationBackend},
dns, dns,
frontend::{FrontendEvent, FrontendListener}, frontend::{FrontendListener, FrontendRequest},
server::capture_task::CaptureEvent, server::capture_task::CaptureEvent,
}; };
@@ -40,7 +40,7 @@ pub struct Server {
client_manager: Rc<RefCell<ClientManager>>, client_manager: Rc<RefCell<ClientManager>>,
port: Rc<Cell<u16>>, port: Rc<Cell<u16>>,
state: Rc<Cell<State>>, state: Rc<Cell<State>>,
release_bind: Vec<crate::scancode::Linux>, release_bind: Vec<input_event::scancode::Linux>,
} }
impl Server { impl Server {
@@ -50,13 +50,22 @@ impl Server {
let state = Rc::new(Cell::new(State::Receiving)); let state = Rc::new(Cell::new(State::Receiving));
let port = Rc::new(Cell::new(config.port)); let port = Rc::new(Cell::new(config.port));
for config_client in config.get_clients() { for config_client in config.get_clients() {
client_manager.borrow_mut().add_client( let client = ClientConfig {
config_client.hostname, hostname: config_client.hostname,
config_client.ips, fix_ips: config_client.ips.into_iter().collect(),
config_client.port, port: config_client.port,
config_client.pos, pos: config_client.pos,
config_client.active, cmd: config_client.enter_hook,
); };
let state = ClientState {
active: config_client.active,
ips: HashSet::from_iter(client.fix_ips.iter().cloned()),
..Default::default()
};
let mut client_manager = client_manager.borrow_mut();
let handle = client_manager.add_client();
let c = client_manager.get_mut(handle).expect("invalid handle");
*c = (client, state);
} }
let release_bind = config.release_bind.clone(); let release_bind = config.release_bind.clone();
Self { Self {
@@ -68,7 +77,11 @@ impl Server {
} }
} }
pub async fn run(&self) -> anyhow::Result<()> { pub async fn run(
&self,
capture_backend: Option<CaptureBackend>,
emulation_backend: Option<EmulationBackend>,
) -> anyhow::Result<()> {
// create frontend communication adapter // create frontend communication adapter
let frontend = match FrontendListener::new().await { let frontend = match FrontendListener::new().await {
Some(f) => f?, Some(f) => f?,
@@ -78,37 +91,37 @@ impl Server {
return anyhow::Ok(()); return anyhow::Ok(());
} }
}; };
let (emulate, capture) = tokio::join!(emulate::create(), capture::create());
let (timer_tx, timer_rx) = tokio::sync::mpsc::channel(1); let (timer_tx, timer_rx) = tokio::sync::mpsc::channel(1);
let (frontend_notify_tx, frontend_notify_rx) = tokio::sync::mpsc::channel(1); let (frontend_notify_tx, frontend_notify_rx) = tokio::sync::mpsc::channel(1);
// udp task // udp task
let (mut udp_task, sender_tx, receiver_rx, port_tx) = let (mut udp_task, sender_tx, receiver_rx, port_tx) =
network_task::new(self.clone(), frontend_notify_tx).await?; network_task::new(self.clone(), frontend_notify_tx.clone()).await?;
// input capture // input capture
let (mut capture_task, capture_channel) = capture_task::new( let (mut capture_task, capture_channel) = capture_task::new(
capture, capture_backend,
self.clone(), self.clone(),
sender_tx.clone(), sender_tx.clone(),
timer_tx.clone(), timer_tx.clone(),
self.release_bind.clone(), self.release_bind.clone(),
); )?;
// input emulation // input emulation
let (mut emulation_task, emulate_channel) = emulation_task::new( let (mut emulation_task, emulate_channel) = emulation_task::new(
emulate, emulation_backend,
self.clone(), self.clone(),
receiver_rx, receiver_rx,
sender_tx.clone(), sender_tx.clone(),
capture_channel.clone(), capture_channel.clone(),
timer_tx, timer_tx,
); )?;
// create dns resolver // create dns resolver
let resolver = dns::DnsResolver::new().await?; let resolver = dns::DnsResolver::new().await?;
let (mut resolver_task, resolve_tx) = resolver_task::new(resolver, self.clone()); let (mut resolver_task, resolve_tx) =
resolver_task::new(resolver, self.clone(), frontend_notify_tx);
// frontend listener // frontend listener
let (mut frontend_task, frontend_tx) = frontend_task::new( let (mut frontend_task, frontend_tx) = frontend_task::new(
@@ -134,9 +147,9 @@ impl Server {
.client_manager .client_manager
.borrow() .borrow()
.get_client_states() .get_client_states()
.filter_map(|s| { .filter_map(|(h, (c, s))| {
if s.active { if s.active {
Some((s.client.handle, s.client.hostname.clone())) Some((h, c.hostname.clone()))
} else { } else {
None None
} }
@@ -144,7 +157,7 @@ impl Server {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
for (handle, hostname) in active { for (handle, hostname) in active {
frontend_tx frontend_tx
.send(FrontendEvent::ActivateClient(handle, true)) .send(FrontendRequest::Activate(handle, true))
.await?; .await?;
if let Some(hostname) = hostname { if let Some(hostname) = hostname {
let _ = resolve_tx.send(DnsRequest { hostname, handle }).await; let _ = resolve_tx.send(DnsRequest { hostname, handle }).await;
@@ -178,7 +191,7 @@ impl Server {
let _ = emulate_channel.send(EmulationEvent::Terminate).await; let _ = emulate_channel.send(EmulationEvent::Terminate).await;
let _ = capture_channel.send(CaptureEvent::Terminate).await; let _ = capture_channel.send(CaptureEvent::Terminate).await;
let _ = frontend_tx.send(FrontendEvent::Shutdown()).await; let _ = frontend_tx.send(FrontendRequest::Terminate()).await;
if !capture_task.is_finished() { if !capture_task.is_finished() {
if let Err(e) = capture_task.await { if let Err(e) = capture_task.await {

View File

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

View File

@@ -6,20 +6,22 @@ use tokio::{
task::JoinHandle, task::JoinHandle,
}; };
use crate::{ use crate::{client::ClientHandle, config::EmulationBackend, server::State};
client::{ClientEvent, ClientHandle}, use input_emulation::{
emulate::InputEmulation, self,
event::{Event, KeyboardEvent}, error::{EmulationCreationError, EmulationError},
scancode, EmulationHandle, InputEmulation,
server::State,
}; };
use input_event::{Event, KeyboardEvent};
use super::{CaptureEvent, Server}; use super::{CaptureEvent, Server};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum EmulationEvent { pub enum EmulationEvent {
/// input emulation is notified of a change in client states /// create a new client
ClientEvent(ClientEvent), Create(EmulationHandle),
/// destroy a client
Destroy(EmulationHandle),
/// input emulation must release keys for client /// input emulation must release keys for client
ReleaseKeys(ClientHandle), ReleaseKeys(ClientHandle),
/// termination signal /// termination signal
@@ -27,36 +29,36 @@ pub enum EmulationEvent {
} }
pub fn new( pub fn new(
mut emulate: Box<dyn InputEmulation>, backend: Option<EmulationBackend>,
server: Server, server: Server,
mut udp_rx: Receiver<Result<(Event, SocketAddr)>>, mut udp_rx: Receiver<Result<(Event, SocketAddr)>>,
sender_tx: Sender<(Event, SocketAddr)>, sender_tx: Sender<(Event, SocketAddr)>,
capture_tx: Sender<CaptureEvent>, capture_tx: Sender<CaptureEvent>,
timer_tx: Sender<()>, timer_tx: Sender<()>,
) -> (JoinHandle<Result<()>>, Sender<EmulationEvent>) { ) -> Result<(JoinHandle<Result<()>>, Sender<EmulationEvent>), EmulationCreationError> {
let (tx, mut rx) = tokio::sync::mpsc::channel(32); let (tx, mut rx) = tokio::sync::mpsc::channel(32);
let emulate_task = tokio::task::spawn_local(async move { let emulate_task = tokio::task::spawn_local(async move {
let backend = backend.map(|b| b.into());
let mut emulate = input_emulation::create(backend).await?;
let mut last_ignored = None; let mut last_ignored = None;
loop { loop {
tokio::select! { tokio::select! {
udp_event = udp_rx.recv() => { udp_event = udp_rx.recv() => {
let udp_event = udp_event.ok_or(anyhow!("receiver closed"))??; let udp_event = udp_event.ok_or(anyhow!("receiver closed"))??;
handle_udp_rx(&server, &capture_tx, &mut emulate, &sender_tx, &mut last_ignored, udp_event, &timer_tx).await; handle_udp_rx(&server, &capture_tx, &mut emulate, &sender_tx, &mut last_ignored, udp_event, &timer_tx).await?;
} }
emulate_event = rx.recv() => { emulate_event = rx.recv() => {
match emulate_event { match emulate_event {
Some(e) => match e { Some(e) => match e {
EmulationEvent::ClientEvent(e) => emulate.notify(e).await, EmulationEvent::Create(h) => emulate.create(h).await,
EmulationEvent::ReleaseKeys(c) => release_keys(&server, &mut emulate, c).await, EmulationEvent::Destroy(h) => emulate.destroy(h).await,
EmulationEvent::ReleaseKeys(c) => release_keys(&server, &mut emulate, c).await?,
EmulationEvent::Terminate => break, EmulationEvent::Terminate => break,
}, },
None => break, None => break,
} }
} }
res = emulate.dispatch() => {
res?;
}
} }
} }
@@ -65,17 +67,15 @@ pub fn new(
.client_manager .client_manager
.borrow() .borrow()
.get_client_states() .get_client_states()
.map(|s| s.client.handle) .map(|(h, _)| h)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
for client in clients { for client in clients {
release_keys(&server, &mut emulate, client).await; release_keys(&server, &mut emulate, client).await?;
} }
// destroy emulator
emulate.destroy().await;
anyhow::Ok(()) anyhow::Ok(())
}); });
(emulate_task, tx) Ok((emulate_task, tx))
} }
async fn handle_udp_rx( async fn handle_udp_rx(
@@ -86,7 +86,7 @@ async fn handle_udp_rx(
last_ignored: &mut Option<SocketAddr>, last_ignored: &mut Option<SocketAddr>,
event: (Event, SocketAddr), event: (Event, SocketAddr),
timer_tx: &Sender<()>, timer_tx: &Sender<()>,
) { ) -> Result<(), EmulationError> {
let (event, addr) = event; let (event, addr) = event;
// get handle for addr // get handle for addr
@@ -97,7 +97,7 @@ async fn handle_udp_rx(
log::warn!("ignoring events from client {addr}"); log::warn!("ignoring events from client {addr}");
last_ignored.replace(addr); last_ignored.replace(addr);
} }
return; return Ok(());
} }
}; };
@@ -108,10 +108,10 @@ async fn handle_udp_rx(
{ {
let mut client_manager = server.client_manager.borrow_mut(); let mut client_manager = server.client_manager.borrow_mut();
let client_state = match client_manager.get_mut(handle) { let client_state = match client_manager.get_mut(handle) {
Some(s) => s, Some((_, s)) => s,
None => { None => {
log::error!("unknown handle"); log::error!("unknown handle");
return; return Ok(());
} }
}; };
@@ -127,7 +127,7 @@ async fn handle_udp_rx(
let _ = sender_tx.send((Event::Pong(), addr)).await; let _ = sender_tx.send((Event::Pong(), addr)).await;
} }
(Event::Disconnect(), _) => { (Event::Disconnect(), _) => {
release_keys(server, emulate, handle).await; release_keys(server, emulate, handle).await?;
} }
(event, addr) => { (event, addr) => {
// tell clients that we are ready to receive events // tell clients that we are ready to receive events
@@ -156,13 +156,12 @@ async fn handle_udp_rx(
}) = event }) = event
{ {
let mut client_manager = server.client_manager.borrow_mut(); let mut client_manager = server.client_manager.borrow_mut();
let client_state = let client_state = if let Some((_, s)) = client_manager.get_mut(handle) {
if let Some(client_state) = client_manager.get_mut(handle) { s
client_state } else {
} else { log::error!("unknown handle");
log::error!("unknown handle"); return Ok(());
return; };
};
if state == 0 { if state == 0 {
// ignore release event if key not pressed // ignore release event if key not pressed
ignore_event = !client_state.pressed_keys.remove(&key); ignore_event = !client_state.pressed_keys.remove(&key);
@@ -176,8 +175,8 @@ async fn handle_udp_rx(
// workaround buggy rdp backend. // workaround buggy rdp backend.
if !ignore_event { if !ignore_event {
// consume event // consume event
emulate.consume(event, handle).await; emulate.consume(event, handle).await?;
log::trace!("{event:?} => emulate"); log::trace!("{event} => emulate");
} }
} }
State::AwaitingLeave => { State::AwaitingLeave => {
@@ -201,19 +200,20 @@ async fn handle_udp_rx(
} }
} }
} }
Ok(())
} }
async fn release_keys( async fn release_keys(
server: &Server, server: &Server,
emulate: &mut Box<dyn InputEmulation>, emulate: &mut Box<dyn InputEmulation>,
client: ClientHandle, client: ClientHandle,
) { ) -> Result<(), EmulationError> {
let keys = server let keys = server
.client_manager .client_manager
.borrow_mut() .borrow_mut()
.get_mut(client) .get_mut(client)
.iter_mut() .iter_mut()
.flat_map(|s| s.pressed_keys.drain()) .flat_map(|(_, s)| s.pressed_keys.drain())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
for key in keys { for key in keys {
@@ -222,19 +222,18 @@ async fn release_keys(
key, key,
state: 0, state: 0,
}); });
emulate.consume(event, client).await; emulate.consume(event, client).await?;
if let Ok(key) = scancode::Linux::try_from(key) { if let Ok(key) = input_event::scancode::Linux::try_from(key) {
log::warn!("releasing stuck key: {key:?}"); log::warn!("releasing stuck key: {key:?}");
} }
} }
let modifiers_event = KeyboardEvent::Modifiers { let event = Event::Keyboard(KeyboardEvent::Modifiers {
mods_depressed: 0, mods_depressed: 0,
mods_latched: 0, mods_latched: 0,
mods_locked: 0, mods_locked: 0,
group: 0, group: 0,
}; });
emulate emulate.consume(event, client).await?;
.consume(Event::Keyboard(modifiers_event), client) Ok(())
.await;
} }

View File

@@ -17,8 +17,8 @@ use tokio::{
}; };
use crate::{ use crate::{
client::{ClientEvent, ClientHandle, Position}, client::{ClientHandle, Position},
frontend::{self, FrontendEvent, FrontendListener, FrontendNotify}, frontend::{self, FrontendEvent, FrontendListener, FrontendRequest},
}; };
use super::{ use super::{
@@ -27,13 +27,13 @@ use super::{
pub(crate) fn new( pub(crate) fn new(
mut frontend: FrontendListener, mut frontend: FrontendListener,
mut notify_rx: Receiver<FrontendNotify>, mut notify_rx: Receiver<FrontendEvent>,
server: Server, server: Server,
capture_notify: Sender<CaptureEvent>, capture: Sender<CaptureEvent>,
emulate_notify: Sender<EmulationEvent>, emulate: Sender<EmulationEvent>,
resolve_ch: Sender<DnsRequest>, resolve_ch: Sender<DnsRequest>,
port_tx: Sender<u16>, port_tx: Sender<u16>,
) -> (JoinHandle<Result<()>>, Sender<FrontendEvent>) { ) -> (JoinHandle<Result<()>>, Sender<FrontendRequest>) {
let (event_tx, mut event_rx) = tokio::sync::mpsc::channel(32); let (event_tx, mut event_rx) = tokio::sync::mpsc::channel(32);
let event_tx_clone = event_tx.clone(); let event_tx_clone = event_tx.clone();
let frontend_task = tokio::task::spawn_local(async move { let frontend_task = tokio::task::spawn_local(async move {
@@ -51,13 +51,13 @@ pub(crate) fn new(
} }
event = event_rx.recv() => { event = event_rx.recv() => {
let frontend_event = event.ok_or(anyhow!("frontend channel closed"))?; let frontend_event = event.ok_or(anyhow!("frontend channel closed"))?;
if handle_frontend_event(&server, &capture_notify, &emulate_notify, &resolve_ch, &mut frontend, &port_tx, frontend_event).await { if handle_frontend_event(&server, &capture, &emulate, &resolve_ch, &mut frontend, &port_tx, frontend_event).await {
break; break;
} }
} }
notify = notify_rx.recv() => { notify = notify_rx.recv() => {
let notify = notify.ok_or(anyhow!("frontend notify closed"))?; let notify = notify.ok_or(anyhow!("frontend notify closed"))?;
let _ = frontend.notify_all(notify).await; let _ = frontend.broadcast_event(notify).await;
} }
} }
} }
@@ -67,7 +67,7 @@ pub(crate) fn new(
} }
async fn handle_frontend_stream( async fn handle_frontend_stream(
frontend_tx: &Sender<FrontendEvent>, frontend_tx: &Sender<FrontendRequest>,
#[cfg(unix)] mut stream: ReadHalf<UnixStream>, #[cfg(unix)] mut stream: ReadHalf<UnixStream>,
#[cfg(windows)] mut stream: ReadHalf<TcpStream>, #[cfg(windows)] mut stream: ReadHalf<TcpStream>,
) { ) {
@@ -75,12 +75,11 @@ async fn handle_frontend_stream(
let tx = frontend_tx.clone(); let tx = frontend_tx.clone();
tokio::task::spawn_local(async move { tokio::task::spawn_local(async move {
let _ = tx.send(FrontendEvent::Enumerate()).await;
loop { loop {
let event = frontend::read_event(&mut stream).await; let request = frontend::wait_for_request(&mut stream).await;
match event { match request {
Ok(event) => { Ok(request) => {
let _ = tx.send(event).await; let _ = tx.send(request).await;
} }
Err(e) => { Err(e) => {
if let Some(e) = e.downcast_ref::<io::Error>() { if let Some(e) = e.downcast_ref::<io::Error>() {
@@ -98,245 +97,254 @@ async fn handle_frontend_stream(
async fn handle_frontend_event( async fn handle_frontend_event(
server: &Server, server: &Server,
capture_tx: &Sender<CaptureEvent>, capture: &Sender<CaptureEvent>,
emulate_tx: &Sender<EmulationEvent>, emulate: &Sender<EmulationEvent>,
resolve_tx: &Sender<DnsRequest>, resolve_tx: &Sender<DnsRequest>,
frontend: &mut FrontendListener, frontend: &mut FrontendListener,
port_tx: &Sender<u16>, port_tx: &Sender<u16>,
event: FrontendEvent, event: FrontendRequest,
) -> bool { ) -> bool {
log::debug!("frontend: {event:?}"); log::debug!("frontend: {event:?}");
let response = match event { match event {
FrontendEvent::AddClient(hostname, port, pos) => { FrontendRequest::Create => {
let handle = add_client(server, resolve_tx, hostname, HashSet::new(), port, pos).await; let handle = add_client(server, frontend).await;
resolve_dns(server, resolve_tx, handle).await;
let client = server
.client_manager
.borrow()
.get(handle)
.unwrap()
.client
.clone();
Some(FrontendNotify::NotifyClientCreate(client))
} }
FrontendEvent::ActivateClient(handle, active) => { FrontendRequest::Activate(handle, active) => {
activate_client(server, capture_tx, emulate_tx, handle, active).await; if active {
Some(FrontendNotify::NotifyClientActivate(handle, active)) activate_client(server, capture, emulate, handle).await;
} else {
deactivate_client(server, capture, emulate, handle).await;
}
} }
FrontendEvent::ChangePort(port) => { FrontendRequest::ChangePort(port) => {
let _ = port_tx.send(port).await; let _ = port_tx.send(port).await;
None
} }
FrontendEvent::DelClient(handle) => { FrontendRequest::Delete(handle) => {
remove_client(server, capture_tx, emulate_tx, frontend, handle).await; remove_client(server, capture, emulate, handle).await;
Some(FrontendNotify::NotifyClientDelete(handle)) broadcast(frontend, FrontendEvent::Deleted(handle)).await;
} }
FrontendEvent::Enumerate() => { FrontendRequest::Enumerate() => {
let clients = server let clients = server
.client_manager .client_manager
.borrow() .borrow()
.get_client_states() .get_client_states()
.map(|s| (s.client.clone(), s.active)) .map(|(h, (c, s))| (h, c.clone(), s.clone()))
.collect(); .collect();
Some(FrontendNotify::Enumerate(clients)) broadcast(frontend, FrontendEvent::Enumerate(clients)).await;
} }
FrontendEvent::Shutdown() => { FrontendRequest::GetState(handle) => {
broadcast_client(server, frontend, handle).await;
}
FrontendRequest::Terminate() => {
log::info!("terminating gracefully..."); log::info!("terminating gracefully...");
return true; return true;
} }
FrontendEvent::UpdateClient(handle, hostname, port, pos) => { FrontendRequest::UpdateFixIps(handle, fix_ips) => {
update_client( update_fix_ips(server, handle, fix_ips).await;
server, resolve_dns(server, resolve_tx, handle).await;
capture_tx, }
emulate_tx, FrontendRequest::UpdateHostname(handle, hostname) => {
resolve_tx, update_hostname(server, resolve_tx, handle, hostname).await;
(handle, hostname, port, pos), resolve_dns(server, resolve_tx, handle).await;
) }
.await; FrontendRequest::UpdatePort(handle, port) => {
update_port(server, handle, port).await;
let client = server }
.client_manager FrontendRequest::UpdatePosition(handle, pos) => {
.borrow() update_pos(server, handle, capture, emulate, pos).await;
.get(handle) }
.unwrap() FrontendRequest::ResolveDns(handle) => {
.client resolve_dns(server, resolve_tx, handle).await;
.clone();
Some(FrontendNotify::NotifyClientUpdate(client))
} }
}; };
let Some(response) = response else {
return false;
};
if let Err(e) = frontend.notify_all(response).await {
log::error!("error notifying frontend: {e}");
}
false false
} }
pub async fn add_client( async fn resolve_dns(server: &Server, resolve_tx: &Sender<DnsRequest>, handle: ClientHandle) {
server: &Server, let hostname = server
resolver_tx: &Sender<DnsRequest>, .client_manager
hostname: Option<String>, .borrow()
addr: HashSet<IpAddr>, .get(handle)
port: u16, .and_then(|(c, _)| c.hostname.clone());
pos: Position,
) -> ClientHandle {
log::info!(
"adding client [{}]{} @ {:?}",
pos,
hostname.as_deref().unwrap_or(""),
&addr
);
let handle =
server
.client_manager
.borrow_mut()
.add_client(hostname.clone(), addr, port, pos, false);
log::debug!("add_client {handle}");
if let Some(hostname) = hostname { if let Some(hostname) = hostname {
let _ = resolver_tx.send(DnsRequest { hostname, handle }).await; let _ = resolve_tx
.send(DnsRequest {
hostname: hostname.clone(),
handle,
})
.await;
} }
}
async fn broadcast(frontend: &mut FrontendListener, event: FrontendEvent) {
if let Err(e) = frontend.broadcast_event(event).await {
log::error!("error notifying frontend: {e}");
}
}
pub async fn add_client(server: &Server, frontend: &mut FrontendListener) -> ClientHandle {
let handle = server.client_manager.borrow_mut().add_client();
log::info!("added client {handle}");
let (c, s) = server.client_manager.borrow().get(handle).unwrap().clone();
broadcast(frontend, FrontendEvent::Created(handle, c, s)).await;
handle handle
} }
pub async fn deactivate_client(
server: &Server,
capture: &Sender<CaptureEvent>,
emulate: &Sender<EmulationEvent>,
handle: ClientHandle,
) {
match server.client_manager.borrow_mut().get_mut(handle) {
Some((_, s)) => {
s.active = false;
}
None => return,
};
let _ = capture.send(CaptureEvent::Destroy(handle)).await;
let _ = emulate.send(EmulationEvent::Destroy(handle)).await;
}
pub async fn activate_client( pub async fn activate_client(
server: &Server, server: &Server,
capture_notify_tx: &Sender<CaptureEvent>, capture: &Sender<CaptureEvent>,
emulate_notify_tx: &Sender<EmulationEvent>, emulate: &Sender<EmulationEvent>,
client: ClientHandle, handle: ClientHandle,
active: bool,
) { ) {
let (client, pos) = match server.client_manager.borrow_mut().get_mut(client) { /* deactivate potential other client at this position */
Some(state) => { let pos = match server.client_manager.borrow().get(handle) {
state.active = active; Some((client, _)) => client.pos,
(state.client.handle, state.client.pos)
}
None => return, None => return,
}; };
if active {
let _ = capture_notify_tx let other = server.client_manager.borrow_mut().find_client(pos);
.send(CaptureEvent::ClientEvent(ClientEvent::Create(client, pos))) if let Some(other) = other {
.await; if other != handle {
let _ = emulate_notify_tx deactivate_client(server, capture, emulate, other).await;
.send(EmulationEvent::ClientEvent(ClientEvent::Create( }
client, pos,
)))
.await;
} else {
let _ = capture_notify_tx
.send(CaptureEvent::ClientEvent(ClientEvent::Destroy(client)))
.await;
let _ = emulate_notify_tx
.send(EmulationEvent::ClientEvent(ClientEvent::Destroy(client)))
.await;
} }
/* activate the client */
if let Some((_, s)) = server.client_manager.borrow_mut().get_mut(handle) {
s.active = true;
} else {
return;
};
/* notify emulation, capture and frontends */
let _ = capture.send(CaptureEvent::Create(handle, pos.into())).await;
let _ = emulate.send(EmulationEvent::Create(handle)).await;
} }
pub async fn remove_client( pub async fn remove_client(
server: &Server, server: &Server,
capture_notify_tx: &Sender<CaptureEvent>, capture: &Sender<CaptureEvent>,
emulate_notify_tx: &Sender<EmulationEvent>, emulate: &Sender<EmulationEvent>,
frontend: &mut FrontendListener, handle: ClientHandle,
client: ClientHandle, ) {
) -> Option<ClientHandle> { let Some(active) = server
let Some((client, active)) = server
.client_manager .client_manager
.borrow_mut() .borrow_mut()
.remove_client(client) .remove_client(handle)
.map(|s| (s.client.handle, s.active)) .map(|(_, s)| s.active)
else { else {
return None; return;
}; };
if active { if active {
let _ = capture_notify_tx let _ = capture.send(CaptureEvent::Destroy(handle)).await;
.send(CaptureEvent::ClientEvent(ClientEvent::Destroy(client))) let _ = emulate.send(EmulationEvent::Destroy(handle)).await;
.await;
let _ = emulate_notify_tx
.send(EmulationEvent::ClientEvent(ClientEvent::Destroy(client)))
.await;
} }
let notify = FrontendNotify::NotifyClientDelete(client);
log::debug!("{notify:?}");
if let Err(e) = frontend.notify_all(notify).await {
log::error!("error notifying frontend: {e}");
}
Some(client)
} }
async fn update_client( async fn update_fix_ips(server: &Server, handle: ClientHandle, fix_ips: Vec<IpAddr>) {
let mut client_manager = server.client_manager.borrow_mut();
let Some((c, _)) = client_manager.get_mut(handle) else {
return;
};
c.fix_ips = fix_ips;
}
async fn update_hostname(
server: &Server, server: &Server,
capture_notify_tx: &Sender<CaptureEvent>,
emulate_notify_tx: &Sender<EmulationEvent>,
resolve_tx: &Sender<DnsRequest>, resolve_tx: &Sender<DnsRequest>,
client_update: (ClientHandle, Option<String>, u16, Position), handle: ClientHandle,
hostname: Option<String>,
) { ) {
let (handle, hostname, port, pos) = client_update; let hostname = {
let mut changed = false;
let (hostname, handle, active) = {
// retrieve state
let mut client_manager = server.client_manager.borrow_mut(); let mut client_manager = server.client_manager.borrow_mut();
let Some(state) = client_manager.get_mut(handle) else { let Some((c, s)) = client_manager.get_mut(handle) else {
return; return;
}; };
// update pos
if state.client.pos != pos {
state.client.pos = pos;
changed = true;
}
// update port
if state.client.port != port {
state.client.port = port;
state.active_addr = state.active_addr.map(|a| SocketAddr::new(a.ip(), port));
changed = true;
}
// update hostname // update hostname
if state.client.hostname != hostname { if c.hostname != hostname {
state.client.ips = HashSet::new(); c.hostname = hostname;
state.active_addr = None; s.ips = HashSet::from_iter(c.fix_ips.iter().cloned());
state.client.hostname = hostname; s.active_addr = None;
changed = true; c.hostname.clone()
} else {
None
} }
log::debug!("client updated: {:?}", state);
(
state.client.hostname.clone(),
state.client.handle,
state.active,
)
}; };
// resolve dns if something changed // resolve to update ips in state
if changed { if let Some(hostname) = hostname {
// resolve dns let _ = resolve_tx.send(DnsRequest { hostname, handle }).await;
if let Some(hostname) = hostname { }
let _ = resolve_tx.send(DnsRequest { hostname, handle }).await; }
}
} async fn update_port(server: &Server, handle: ClientHandle, port: u16) {
let mut client_manager = server.client_manager.borrow_mut();
// update state in event input emulator & input capture let Some((c, s)) = client_manager.get_mut(handle) else {
if changed && active { return;
// update state };
let _ = capture_notify_tx
.send(CaptureEvent::ClientEvent(ClientEvent::Destroy(handle))) if c.port != port {
.await; c.port = port;
let _ = emulate_notify_tx s.active_addr = s.active_addr.map(|a| SocketAddr::new(a.ip(), port));
.send(EmulationEvent::ClientEvent(ClientEvent::Destroy(handle))) }
.await; }
let _ = capture_notify_tx
.send(CaptureEvent::ClientEvent(ClientEvent::Create(handle, pos))) async fn update_pos(
.await; server: &Server,
let _ = emulate_notify_tx handle: ClientHandle,
.send(EmulationEvent::ClientEvent(ClientEvent::Create( capture: &Sender<CaptureEvent>,
handle, pos, emulate: &Sender<EmulationEvent>,
))) pos: Position,
.await; ) {
let (changed, active) = {
let mut client_manager = server.client_manager.borrow_mut();
let Some((c, s)) = client_manager.get_mut(handle) else {
return;
};
let changed = c.pos != pos;
c.pos = pos;
(changed, s.active)
};
// update state in event input emulator & input capture
if changed {
if active {
let _ = capture.send(CaptureEvent::Destroy(handle)).await;
let _ = emulate.send(EmulationEvent::Destroy(handle)).await;
}
let _ = capture.send(CaptureEvent::Create(handle, pos.into())).await;
let _ = emulate.send(EmulationEvent::Create(handle)).await;
}
}
async fn broadcast_client(server: &Server, frontend: &mut FrontendListener, handle: ClientHandle) {
let client = server.client_manager.borrow().get(handle).cloned();
if let Some((config, state)) = client {
broadcast(frontend, FrontendEvent::State(handle, config, state)).await;
} else {
broadcast(frontend, FrontendEvent::NoSuchClient(handle)).await;
} }
} }

View File

@@ -7,13 +7,14 @@ use tokio::{
task::JoinHandle, task::JoinHandle,
}; };
use crate::{event::Event, frontend::FrontendNotify}; use crate::frontend::FrontendEvent;
use input_event::Event;
use super::Server; use super::Server;
pub async fn new( pub async fn new(
server: Server, server: Server,
frontend_notify_tx: Sender<FrontendNotify>, frontend_notify_tx: Sender<FrontendEvent>,
) -> Result<( ) -> Result<(
JoinHandle<()>, JoinHandle<()>,
Sender<(Event, SocketAddr)>, Sender<(Event, SocketAddr)>,
@@ -55,12 +56,12 @@ pub async fn new(
Ok(new_socket) => { Ok(new_socket) => {
socket = new_socket; socket = new_socket;
server.port.replace(port); server.port.replace(port);
let _ = frontend_notify_tx.send(FrontendNotify::NotifyPortChange(port, None)).await; let _ = frontend_notify_tx.send(FrontendEvent::PortChanged(port, None)).await;
} }
Err(e) => { Err(e) => {
log::warn!("could not change port: {e}"); log::warn!("could not change port: {e}");
let port = socket.local_addr().unwrap().port(); let port = socket.local_addr().unwrap().port();
let _ = frontend_notify_tx.send(FrontendNotify::NotifyPortChange( let _ = frontend_notify_tx.send(FrontendEvent::PortChanged(
port, port,
Some(format!("could not change port: {e}")), Some(format!("could not change port: {e}")),
)).await; )).await;

View File

@@ -5,7 +5,9 @@ use tokio::{
task::JoinHandle, task::JoinHandle,
}; };
use crate::{client::ClientHandle, event::Event}; use input_event::Event;
use crate::client::ClientHandle;
use super::{capture_task::CaptureEvent, emulation_task::EmulationEvent, Server, State}; use super::{capture_task::CaptureEvent, emulation_task::EmulationEvent, Server, State};
@@ -34,8 +36,8 @@ pub fn new(
// if receiving we care about clients with pressed keys // if receiving we care about clients with pressed keys
client_manager client_manager
.get_client_states_mut() .get_client_states_mut()
.filter(|s| !s.pressed_keys.is_empty()) .filter(|(_, (_, s))| !s.pressed_keys.is_empty())
.map(|s| s.client.handle) .map(|(h, _)| h)
.collect() .collect()
} else { } else {
// if sending we care about the active client // if sending we care about the active client
@@ -46,17 +48,15 @@ pub fn new(
let ping_addrs: Vec<SocketAddr> = { let ping_addrs: Vec<SocketAddr> = {
ping_clients ping_clients
.iter() .iter()
.flat_map(|&c| client_manager.get(c)) .flat_map(|&h| client_manager.get(h))
.flat_map(|state| { .flat_map(|(c, s)| {
if state.alive && state.active_addr.is_some() { if s.alive && s.active_addr.is_some() {
vec![state.active_addr.unwrap()] vec![s.active_addr.unwrap()]
} else { } else {
state s.ips
.client
.ips
.iter() .iter()
.cloned() .cloned()
.map(|ip| SocketAddr::new(ip, state.client.port)) .map(|ip| SocketAddr::new(ip, c.port))
.collect() .collect()
} }
}) })
@@ -64,8 +64,8 @@ pub fn new(
}; };
// reset alive // reset alive
for state in client_manager.get_client_states_mut() { for (_, (_, s)) in client_manager.get_client_states_mut() {
state.alive = false; s.alive = false;
} }
(ping_clients, ping_addrs) (ping_clients, ping_addrs)
@@ -102,8 +102,8 @@ pub fn new(
let client_manager = server.client_manager.borrow(); let client_manager = server.client_manager.borrow();
ping_clients ping_clients
.iter() .iter()
.filter_map(|&c| match client_manager.get(c) { .filter_map(|&h| match client_manager.get(h) {
Some(state) if !state.alive => Some(c), Some((_, s)) if !s.alive => Some(h),
_ => None, _ => None,
}) })
.collect() .collect()
@@ -112,9 +112,9 @@ pub fn new(
// we may not be receiving anymore but we should respond // we may not be receiving anymore but we should respond
// to the original state and not the "new" one // to the original state and not the "new" one
if receiving { if receiving {
for c in unresponsive_clients { for h in unresponsive_clients {
log::warn!("device not responding, releasing keys!"); log::warn!("device not responding, releasing keys!");
let _ = emulate_notify.send(EmulationEvent::ReleaseKeys(c)).await; let _ = emulate_notify.send(EmulationEvent::ReleaseKeys(h)).await;
} }
} else { } else {
// release pointer if the active client has not responded // release pointer if the active client has not responded

View File

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