Compare commits

..

3 Commits

Author SHA1 Message Date
Ferdinand Schober
2594a83e6a formatting 2024-01-02 12:43:33 +01:00
Ferdinand Schober
b81e5806ab basic input event decoding 2024-01-02 12:43:33 +01:00
Ferdinand Schober
a129e27a26 start working on x11 producer 2024-01-02 12:43:33 +01:00
65 changed files with 2268 additions and 3402 deletions

1
.envrc
View File

@@ -1 +0,0 @@
use flake

1
.github/FUNDING.yml vendored
View File

@@ -1 +0,0 @@
github: [feschber]

View File

@@ -1,24 +0,0 @@
name: Binary Cache
on: [push, pull_request, workflow_dispatch]
jobs:
nix:
name: "Build"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
- uses: DeterminateSystems/nix-installer-action@main
with:
logger: pretty
- uses: DeterminateSystems/magic-nix-cache-action@main
- uses: cachix/cachix-action@v14
with:
name: lan-mouse
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Build lan-mouse
run: nix build --print-build-logs

View File

@@ -13,7 +13,7 @@ jobs:
linux-release-build: linux-release-build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: install dependencies - name: install dependencies
run: | run: |
sudo apt-get update sudo apt-get update
@@ -22,7 +22,7 @@ jobs:
- name: Release Build - name: Release Build
run: cargo build --release run: cargo build --release
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: lan-mouse-linux name: lan-mouse-linux
path: target/release/lan-mouse path: target/release/lan-mouse
@@ -62,7 +62,7 @@ jobs:
gvsbuild build --msys-dir=C:\msys64 gtk4 libadwaita librsvg gvsbuild build --msys-dir=C:\msys64 gtk4 libadwaita librsvg
Move-Item "C:\Program Files\Git\usr\notbin" "C:\Program Files\Git\usr\bin" Move-Item "C:\Program Files\Git\usr\notbin" "C:\Program Files\Git\usr\bin"
Move-Item "C:\Program Files\Git\notbin" "C:\Program Files\Git\bin" Move-Item "C:\Program Files\Git\notbin" "C:\Program Files\Git\bin"
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Release Build - name: Release Build
run: cargo build --release run: cargo build --release
- name: Create Archive - name: Create Archive
@@ -72,7 +72,7 @@ jobs:
Copy-Item -Path "target\release\lan-mouse.exe" -Destination "lan-mouse-windows" Copy-Item -Path "target\release\lan-mouse.exe" -Destination "lan-mouse-windows"
Compress-Archive -Path "lan-mouse-windows\*" -DestinationPath lan-mouse-windows.zip Compress-Archive -Path "lan-mouse-windows\*" -DestinationPath lan-mouse-windows.zip
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: lan-mouse-windows name: lan-mouse-windows
path: lan-mouse-windows.zip path: lan-mouse-windows.zip
@@ -80,7 +80,7 @@ jobs:
macos-release-build: macos-release-build:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: install dependencies - name: install dependencies
run: brew install gtk4 libadwaita run: brew install gtk4 libadwaita
- name: Release Build - name: Release Build
@@ -88,34 +88,18 @@ jobs:
cargo build --release cargo build --release
cp target/release/lan-mouse lan-mouse-macos-intel cp target/release/lan-mouse lan-mouse-macos-intel
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: lan-mouse-macos-intel name: lan-mouse-macos
path: lan-mouse-macos-intel path: lan-mouse-macos-intel
macos-aarch64-release-build:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: install dependencies
run: brew install gtk4 libadwaita
- name: Release Build
run: |
cargo build --release
cp target/release/lan-mouse lan-mouse-macos-aarch64
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: lan-mouse-macos-aarch64
path: lan-mouse-macos-aarch64
pre-release: pre-release:
name: "Pre Release" name: "Pre Release"
needs: [windows-release-build, linux-release-build, macos-release-build] needs: [windows-release-build, linux-release-build, macos-release-build]
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"
steps: steps:
- name: Download build artifacts - name: Download build artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v3
- name: Create Release - name: Create Release
uses: "marvinpinto/action-automatic-releases@latest" uses: "marvinpinto/action-automatic-releases@latest"
with: with:
@@ -125,6 +109,5 @@ jobs:
title: "Development Build" title: "Development Build"
files: | files: |
lan-mouse-linux/lan-mouse lan-mouse-linux/lan-mouse
lan-mouse-macos-intel/lan-mouse-macos-intel lan-mouse-macos/lan-mouse-macos-intel
lan-mouse-macos-aarch64/lan-mouse-macos-aarch64
lan-mouse-windows/lan-mouse-windows.zip lan-mouse-windows/lan-mouse-windows.zip

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: install dependencies - name: install dependencies
run: | run: |
sudo apt-get update sudo apt-get update
@@ -30,7 +30,7 @@ jobs:
- name: Clippy - name: Clippy
run: cargo clippy --all-features --all-targets -- --deny warnings run: cargo clippy --all-features --all-targets -- --deny warnings
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: lan-mouse name: lan-mouse
path: target/debug/lan-mouse path: target/debug/lan-mouse
@@ -40,7 +40,7 @@ jobs:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
python-version: '3.11' python-version: '3.11'
@@ -84,7 +84,7 @@ jobs:
- name: Copy Gtk Dlls - name: Copy Gtk Dlls
run: Get-Childitem -Path "C:\\gtk-build\\gtk\\x64\\release\\bin\\*.dll" -File -Recurse | Copy-Item -Destination "target\\debug" run: Get-Childitem -Path "C:\\gtk-build\\gtk\\x64\\release\\bin\\*.dll" -File -Recurse | Copy-Item -Destination "target\\debug"
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: lan-mouse-windows name: lan-mouse-windows
path: | path: |
@@ -94,7 +94,7 @@ jobs:
build-macos: build-macos:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: install dependencies - name: install dependencies
run: brew install gtk4 libadwaita run: brew install gtk4 libadwaita
- name: Build - name: Build
@@ -106,27 +106,7 @@ jobs:
- name: Clippy - name: Clippy
run: cargo clippy --all-features --all-targets -- --deny warnings run: cargo clippy --all-features --all-targets -- --deny warnings
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: lan-mouse-macos name: lan-mouse-macos
path: target/debug/lan-mouse path: target/debug/lan-mouse
build-macos-aarch64:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: install dependencies
run: brew install gtk4 libadwaita
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- name: Check Formatting
run: cargo fmt --check
- name: Clippy
run: cargo clippy --all-features --all-targets -- --deny warnings
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: lan-mouse-macos-aarch64
path: target/debug/lan-mouse

View File

@@ -9,7 +9,7 @@ jobs:
linux-release-build: linux-release-build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: install dependencies - name: install dependencies
run: | run: |
sudo apt-get update sudo apt-get update
@@ -18,7 +18,7 @@ jobs:
- name: Release Build - name: Release Build
run: cargo build --release run: cargo build --release
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: lan-mouse-linux name: lan-mouse-linux
path: target/release/lan-mouse path: target/release/lan-mouse
@@ -58,7 +58,7 @@ jobs:
gvsbuild build --msys-dir=C:\msys64 gtk4 libadwaita librsvg gvsbuild build --msys-dir=C:\msys64 gtk4 libadwaita librsvg
Move-Item "C:\Program Files\Git\usr\notbin" "C:\Program Files\Git\usr\bin" Move-Item "C:\Program Files\Git\usr\notbin" "C:\Program Files\Git\usr\bin"
Move-Item "C:\Program Files\Git\notbin" "C:\Program Files\Git\bin" Move-Item "C:\Program Files\Git\notbin" "C:\Program Files\Git\bin"
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Release Build - name: Release Build
run: cargo build --release run: cargo build --release
- name: Create Archive - name: Create Archive
@@ -68,7 +68,7 @@ jobs:
Copy-Item -Path "target\release\lan-mouse.exe" -Destination "lan-mouse-windows" Copy-Item -Path "target\release\lan-mouse.exe" -Destination "lan-mouse-windows"
Compress-Archive -Path "lan-mouse-windows\*" -DestinationPath lan-mouse-windows.zip Compress-Archive -Path "lan-mouse-windows\*" -DestinationPath lan-mouse-windows.zip
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: lan-mouse-windows name: lan-mouse-windows
path: lan-mouse-windows.zip path: lan-mouse-windows.zip
@@ -76,7 +76,7 @@ jobs:
macos-release-build: macos-release-build:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: install dependencies - name: install dependencies
run: brew install gtk4 libadwaita run: brew install gtk4 libadwaita
- name: Release Build - name: Release Build
@@ -84,34 +84,18 @@ jobs:
cargo build --release cargo build --release
cp target/release/lan-mouse lan-mouse-macos-intel cp target/release/lan-mouse lan-mouse-macos-intel
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: lan-mouse-macos-intel name: lan-mouse-macos
path: lan-mouse-macos-intel path: lan-mouse-macos-intel
macos-aarch64-release-build:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: install dependencies
run: brew install gtk4 libadwaita
- name: Release Build
run: |
cargo build --release
cp target/release/lan-mouse lan-mouse-macos-aarch64
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: lan-mouse-macos-aarch64
path: lan-mouse-macos-aarch64
tagged-release: tagged-release:
name: "Tagged Release" name: "Tagged Release"
needs: [windows-release-build, linux-release-build, macos-release-build] needs: [windows-release-build, linux-release-build, macos-release-build]
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"
steps: steps:
- name: Download build artifacts - name: Download build artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v3
- name: "Create Release" - name: "Create Release"
uses: "marvinpinto/action-automatic-releases@latest" uses: "marvinpinto/action-automatic-releases@latest"
with: with:
@@ -119,6 +103,5 @@ jobs:
prerelease: false prerelease: false
files: | files: |
lan-mouse-linux/lan-mouse lan-mouse-linux/lan-mouse
lan-mouse-macos-intel/lan-mouse-macos-intel lan-mouse-macos/lan-mouse-macos-intel
lan-mouse-macos-aarch64/lan-mouse-macos-aarch64
lan-mouse-windows/lan-mouse-windows.zip lan-mouse-windows/lan-mouse-windows.zip

3
.gitignore vendored
View File

@@ -2,6 +2,3 @@
.gdbinit .gdbinit
.idea/ .idea/
.vs/ .vs/
.vscode/
.direnv/
result

1003
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
[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.5.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"
@@ -20,18 +20,16 @@ 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.10.0"
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", "macros", "net", "rt", "sync", "signal"] }
async-trait = "0.1.73" async-trait = "0.1.73"
futures-core = "0.3.28" 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.7.2", features = ["v4_2"], optional = true }
adw = { package = "libadwaita", version = "0.6.0", features = ["v1_1"], optional = true } adw = { package = "libadwaita", version = "0.5.2", features = ["v1_1"], optional = true }
async-channel = { version = "2.1.1", optional = true } async-channel = { version = "2.1.1", optional = true }
keycode = "0.4.0"
once_cell = "1.19.0"
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
libc = "0.2.148" libc = "0.2.148"
@@ -42,17 +40,17 @@ wayland-protocols = { version="0.31.0", features=["client", "staging", "unstable
wayland-protocols-wlr = { version="0.2.0", features=["client"], optional = true } wayland-protocols-wlr = { version="0.2.0", features=["client"], optional = true }
wayland-protocols-misc = { 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 } x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
ashpd = { version = "0.8", default-features = false, features = ["tokio"], optional = true } ashpd = { version = "0.6.2", default-features = false, features = ["tokio"], optional = true }
reis = { version = "0.2", features = [ "tokio" ], optional = true } reis = { git = "https://github.com/ids1024/reis", features = [ "tokio" ], optional = true }
[target.'cfg(target_os="macos")'.dependencies] [target.'cfg(target_os="macos")'.dependencies]
core-graphics = { version = "0.23", features = ["highsierra"] } core-graphics = { version = "0.23", features = ["highsierra"] }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
windows = { version = "0.54.0", features = [ "Win32_UI_Input_KeyboardAndMouse" ] } winapi = { version = "0.3.9", features = ["winuser"] }
[build-dependencies] [build-dependencies]
glib-build-tools = "0.19.0" glib-build-tools = "0.18.0"
[features] [features]
default = ["wayland", "x11", "xdg_desktop_portal", "libei", "gtk"] default = ["wayland", "x11", "xdg_desktop_portal", "libei", "gtk"]

179
README.md
View File

@@ -14,7 +14,7 @@ The primary target is Wayland on Linux but Windows and MacOS and Linux on Xorg h
</picture> </picture>
Goal of this project is to be an open-source replacement for proprietary tools like [Synergy 2/3](https://symless.com/synergy), [Share Mouse](https://www.sharemouse.com/de/). Goal of this project is to be an open-source replacement for proprietary tools like [Synergy](https://symless.com/synergy), [Share Mouse](https://www.sharemouse.com/de/).
Focus lies on performance and a clean, manageable implementation that can easily be expanded to support additional backends like e.g. Android, iOS, ... . Focus lies on performance and a clean, manageable implementation that can easily be expanded to support additional backends like e.g. Android, iOS, ... .
@@ -22,18 +22,6 @@ Focus lies on performance and a clean, manageable implementation that can easily
For an alternative (with slightly different goals) you may check out [Input Leap](https://github.com/input-leap). For an alternative (with slightly different goals) you may check out [Input Leap](https://github.com/input-leap).
> [!WARNING]
> Since this tool has gained a bit of popularity over the past couple of days:
>
> All network traffic is currently **unencrypted** and sent in **plaintext**.
>
> A malicious actor with access to the network could read input data or send input events with spoofed IPs to take control over a device.
>
> Therefore you should only use this tool in your local network with trusted devices for now
> and I take no responsibility for any leakage of data!
## OS Support ## OS Support
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
@@ -43,102 +31,16 @@ input capture (to send events *to* other clients) on different operating systems
|---------------------------|--------------------------|--------------------------------------| |---------------------------|--------------------------|--------------------------------------|
| 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: | WIP |
| X11 | :heavy_check_mark: | WIP | | X11 | :heavy_check_mark: | WIP |
| Windows | :heavy_check_mark: | WIP | | Windows | :heavy_check_mark: | WIP |
| MacOS | :heavy_check_mark: | WIP | | MacOS | ( :heavy_check_mark: ) | WIP |
> [!Important] Keycode translation is not yet implemented so on MacOS only mouse emulation works as of right now.
> Gnome -> Sway only partially works (modifier events are not handled correctly)
> [!Important] ## Build and Run
> **Wayfire**
>
> If you are using [Wayfire](https://github.com/WayfireWM/wayfire), make sure to use a recent version (must be newer than October 23rd) and **add `shortcuts-inhibit` to the list of plugins in your wayfire config!**
> Otherwise input capture will not work.
## Installation
### Install with cargo
```sh
cargo install lan-mouse
```
### 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).
For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies).
### Arch Linux
Lan Mouse is available on the AUR:
```sh
# git version (includes latest changes)
paru -S lan-mouse-git
# alternatively
paru -S lan-mouse-bin
```
### Nix
- nixpkgs: [search.nixos.org](https://search.nixos.org/packages?channel=unstable&show=lan-mouse&from=0&size=50&sort=relevance&type=packages&query=lan-mouse)
- flake: [README.md](./nix/README.md)
### Building from Source
Build in release mode:
```sh
cargo build --release
```
Run directly:
```sh
cargo run --release
```
Install the files:
```sh
# install lan-mouse
sudo cp target/release/lan-mouse /usr/local/bin/
# install app icon
sudo mkdir -p /usr/local/share/icons/hicolor/scalable/apps
sudo cp resources/de.feschber.LanMouse.svg /usr/local/share/icons/hicolor/scalable/apps
# update icon cache
gtk-update-icon-cache /usr/local/share/icons/hicolor/
# install desktop entry
sudo mkdir -p /usr/local/share/applications
sudo cp de.feschber.LanMouse.desktop /usr/local/share/applications
# when using firewalld: install firewall rule
sudo cp firewall/lan-mouse.xml /etc/firewalld/services
# -> enable the service in firewalld settings
```
### Conditional Compilation
Currently only x11, wayland, windows and MacOS are supported backends.
Depending on the toolchain used, support for other platforms is omitted
automatically (it does not make sense to build a Windows `.exe` with
support for x11 and wayland backends).
However one might still want to omit support for e.g. wayland, x11 or libei on
a Linux system.
This is possible through
[cargo features](https://doc.rust-lang.org/cargo/reference/features.html).
E.g. if only wayland support is needed, the following command produces
an executable with just support for wayland:
```sh
cargo build --no-default-features --features wayland
```
For a detailed list of available features, checkout the [Cargo.toml](./Cargo.toml)
## Installing Dependencies
### Install Dependencies
#### Macos #### Macos
```sh ```sh
brew install libadwaita brew install libadwaita
@@ -160,9 +62,6 @@ sudo dnf install libadwaita-devel libXtst-devel libX11-devel
``` ```
#### Windows #### Windows
> [!NOTE]
> 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) Follow the instructions at [gtk-rs.org](https://gtk-rs.org/gtk4-rs/stable/latest/book/installation_windows.html)
*TLDR:* *TLDR:*
@@ -174,7 +73,7 @@ Build gtk from source
# 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 python 3.11 (Version is important, as 3.12 does not work currently)
choco install python --version=3.11.0 choco install python --version=3.11.0
# install git # install git
@@ -193,10 +92,6 @@ choco install visualstudio2022-workload-vctools
# install gvsbuild with python # install gvsbuild with python
python -m pip install --user pipx python -m pip install --user pipx
python -m pipx ensurepath python -m pipx ensurepath
```
- Relaunch your powershell instance so the changes in the environment are reflected.
```sh
pipx install gvsbuild pipx install gvsbuild
# build gtk + libadwaita # build gtk + libadwaita
@@ -209,6 +104,37 @@ Make sure to add the directory `C:\gtk-build\gtk\x64\release\bin`
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).
### Build and run
Build in release mode:
```sh
cargo build --release
```
Run directly:
```sh
cargo run --release
```
### Conditional Compilation
Currently only x11, wayland, windows and MacOS are supported backends.
Depending on the toolchain used, support for other platforms is omitted
automatically (it does not make sense to build a Windows `.exe` with
support for x11 and wayland backends).
However one might still want to omit support for e.g. wayland, x11 or libei on
a Linux system.
This is possible through
[cargo features](https://doc.rust-lang.org/cargo/reference/features.html).
E.g. if only wayland support is needed, the following command produces
an executable with just support for wayland:
```sh
cargo build --no-default-features --features wayland
```
For a detailed list of available features, checkout the [Cargo.toml](./Cargo.toml)
## Usage ## Usage
### Gtk Frontend ### Gtk Frontend
By default the gtk frontend will open when running `lan-mouse`. By default the gtk frontend will open when running `lan-mouse`.
@@ -242,17 +168,6 @@ To do so, add `--daemon` to the commandline args:
$ cargo run --release -- --daemon $ cargo run --release -- --daemon
``` ```
In order to start lan-mouse with a graphical session automatically,
the [systemd-service](service/lan-mouse.service) can be used:
Copy the file to `~/.config/systemd/user/` and enable the service:
```sh
cp service/lan-mouse.service ~/.config/systemd/user
systemctl --user daemon-reload
systemctl --user enable --now lan-mouse.service
```
## Configuration ## Configuration
To automatically load clients on startup, the file `$XDG_CONFIG_HOME/lan-mouse/config.toml` is parsed. To automatically load clients on startup, the file `$XDG_CONFIG_HOME/lan-mouse/config.toml` is parsed.
`$XDG_CONFIG_HOME` defaults to `~/.config/`. `$XDG_CONFIG_HOME` defaults to `~/.config/`.
@@ -260,17 +175,10 @@ To automatically load clients on startup, the file `$XDG_CONFIG_HOME/lan-mouse/c
To create this file you can copy the following example config: To create this file you can copy the following example config:
### Example config ### Example config
> [!TIP]
> key symbols in the release bind are named according
> to their names in [src/scancode.rs#L172](src/scancode.rs#L172).
> This is bound to change
```toml ```toml
# example configuration # example configuration
# configure release bind
release_bind = [ "KeyA", "KeyS", "KeyD", "KeyF" ]
# optional port (defaults to 4242) # optional port (defaults to 4242)
port = 4242 port = 4242
# # optional frontend -> defaults to gtk if available # # optional frontend -> defaults to gtk if available
@@ -280,9 +188,7 @@ port = 4242
# define a client on the right side with host name "iridium" # define a client on the right side with host name "iridium"
[right] [right]
# hostname # hostname
hostname = "iridium" host_name = "iridium"
# activate this client immediately when lan-mouse is started
activate_on_startup = true
# optional list of (known) ip addresses # optional list of (known) ip addresses
ips = ["192.168.178.156"] ips = ["192.168.178.156"]
@@ -290,7 +196,7 @@ ips = ["192.168.178.156"]
[left] [left]
# The hostname is optional: When no hostname is specified, # The hostname is optional: When no hostname is specified,
# at least one ip address needs to be specified. # at least one ip address needs to be specified.
hostname = "thorium" host_name = "thorium"
# ips for ethernet and wifi # ips for ethernet and wifi
ips = ["192.168.178.189", "192.168.178.172"] ips = ["192.168.178.189", "192.168.178.172"]
# optional port # optional port
@@ -304,12 +210,11 @@ Where `left` can be either `left`, `right`, `top` or `bottom`.
- [x] respect xdg-config-home for config file location. - [x] respect xdg-config-home for config file location.
- [x] IP Address switching - [x] IP Address switching
- [x] Liveness tracking Automatically ungrab mouse when client unreachable - [x] Liveness tracking Automatically ungrab mouse when client unreachable
- [x] Liveness tracking: Automatically release keys, when server offline - [ ] Liveness tracking: Automatically release keys, when server offline
- [x] MacOS KeyCode Translation
- [x] Libei Input Capture
- [ ] X11 Input Capture - [ ] X11 Input Capture
- [ ] Windows Input Capture - [ ] Windows Input Capture
- [ ] MacOS Input Capture - [ ] MacOS Input Capture
- [ ] MaxOS KeyCode Translation
- [ ] Latency measurement and visualization - [ ] Latency measurement and visualization
- [ ] Bandwidth usage measurement and visualization - [ ] Bandwidth usage measurement and visualization
- [ ] Clipboard support - [ ] Clipboard support

View File

@@ -1,8 +1,5 @@
# example configuration # example configuration
# release bind
release_bind = [ "KeyA", "KeyS", "KeyD", "KeyF" ]
# optional port (defaults to 4242) # optional port (defaults to 4242)
port = 4242 port = 4242
# optional frontend -> defaults to gtk if available # optional frontend -> defaults to gtk if available
@@ -11,7 +8,7 @@ port = 4242
# define a client on the right side with host name "iridium" # define a client on the right side with host name "iridium"
[right] [right]
# hostname # hostname
hostname = "iridium" host_name = "iridium"
# optional list of (known) ip addresses # optional list of (known) ip addresses
ips = ["192.168.178.156"] ips = ["192.168.178.156"]
@@ -19,7 +16,7 @@ ips = ["192.168.178.156"]
[left] [left]
# The hostname is optional: When no hostname is specified, # The hostname is optional: When no hostname is specified,
# at least one ip address needs to be specified. # at least one ip address needs to be specified.
hostname = "thorium" host_name = "thorium"
# ips for ethernet and wifi # ips for ethernet and wifi
ips = ["192.168.178.189", "192.168.178.172"] ips = ["192.168.178.189", "192.168.178.172"]
# optional port # optional port

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- for packaging: /usr/lib/firewalld/services/lan-mouse.xml -->
<!-- configure manually: /etc/firewalld/services/lan-mouse.xml -->
<service>
<short>LAN Mouse</short>
<description>mouse and keyboard sharing via LAN</description>
<port port="4242" protocol="udp"/>
</service>

82
flake.lock generated
View File

@@ -1,82 +0,0 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1705309234,
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1710806803,
"narHash": "sha256-qrxvLS888pNJFwJdK+hf1wpRCSQcqA6W5+Ox202NDa0=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "b06025f1533a1e07b6db3e75151caa155d1c7eb3",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1710987136,
"narHash": "sha256-Q8GRdlAIKZ8tJUXrbcRO1pA33AdoPfTUirsSnmGQnOU=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "97596b54ac34ad8184ca1eef44b1ec2e5c2b5f9e",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -1,58 +0,0 @@
{
description = "Nix Flake for lan-mouse";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = {
self,
nixpkgs,
rust-overlay,
...
}: let
inherit (nixpkgs) lib;
genSystems = lib.genAttrs [
"x86_64-linux"
];
pkgsFor = system:
import nixpkgs {
inherit system;
overlays = [
rust-overlay.overlays.default
];
};
mkRustToolchain = pkgs:
pkgs.rust-bin.stable.latest.default.override {
extensions = ["rust-src"];
};
pkgs = genSystems (system: import nixpkgs {inherit system;});
in {
packages = genSystems (system: rec {
default = pkgs.${system}.callPackage ./nix {};
lan-mouse = default;
});
homeManagerModules.default = import ./nix/hm-module.nix self;
devShells = genSystems (system: let
pkgs = pkgsFor system;
rust = mkRustToolchain pkgs;
in {
default = pkgs.mkShell {
packages = with pkgs; [
rust
rust-analyzer-unwrapped
pkg-config
xorg.libX11
gtk4
libadwaita
xorg.libXtst
];
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
};
});
};
}

View File

@@ -1,12 +1,13 @@
[Desktop Entry] [Desktop Entry]
Categories=Utility; Categories=Utility;
Comment[en_US]=Mouse & Keyboard sharing via LAN Comment[en_US]=mouse & keyboard sharing via LAN
Comment=Mouse & Keyboard sharing via LAN
Comment[de_DE]=Maus- und Tastaturfreigabe über LAN Comment[de_DE]=Maus- und Tastaturfreigabe über LAN
Exec=lan-mouse Exec=lan-mouse
Icon=de.feschber.LanMouse Icon=mouse-icon.svg
Name[en_US]=Lan Mouse Name[en_US]=Lan Mouse
Name[de_DE]=Lan Maus
Name=Lan Mouse Name=Lan Mouse
StartupNotify=true StartupNotify=true
Terminal=false Terminal=false
Type=Application Type=Application
Version=0.5.0

View File

@@ -1,40 +0,0 @@
# Nix Flake Usage
## run
```bash
nix run github:feschber/lan-mouse
# with params
nix run github:feschber/lan-mouse -- --help
```
## home-manager module
add input
```nix
inputs = {
lan-mouse.url = "github:feschber/lan-mouse";
}
```
enable lan-mouse
``` nix
{
inputs,
...
}: {
# add the home manager module
imports = [inputs.lan-mouse.homeManagerModules.default];
programs.lan-mouse = {
enable = true;
# systemd = false;
# package = inputs.lan-mouse.packages.${pkgs.stdenv.hostPlatform.system}.default
};
}
```

View File

@@ -1,42 +0,0 @@
{
rustPlatform,
lib,
pkgs,
}:
rustPlatform.buildRustPackage {
pname = "lan-mouse";
version = "0.7.0";
nativeBuildInputs = with pkgs; [
pkg-config
cmake
buildPackages.gtk4
];
buildInputs = with pkgs; [
xorg.libX11
gtk4
libadwaita
xorg.libXtst
];
src = builtins.path {
name = "lan-mouse";
path = lib.cleanSource ../.;
};
cargoLock.lockFile = ../Cargo.lock;
# Set Environment Variables
RUST_BACKTRACE = "full";
meta = with lib; {
description = "Lan Mouse is a mouse and keyboard sharing software";
longDescription = ''
Lan Mouse is a mouse and keyboard sharing software similar to universal-control on Apple devices. It allows for using multiple pcs with a single set of mouse and keyboard. This is also known as a Software KVM switch.
The primary target is Wayland on Linux but Windows and MacOS and Linux on Xorg have partial support as well (see below for more details).
'';
mainProgram = "lan-mouse";
platforms = platforms.all;
};
}

View File

@@ -1,50 +0,0 @@
self: {
config,
pkgs,
lib,
...
}:
with lib; let
cfg = config.programs.lan-mouse;
defaultPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.default;
in {
options.programs.lan-mouse = with types; {
enable = mkEnableOption "Whether or not to enable lan-mouse.";
package = mkOption {
type = with types; nullOr package;
default = defaultPackage;
defaultText = literalExpression "inputs.lan-mouse.packages.${pkgs.stdenv.hostPlatform.system}.default";
description = ''
The lan-mouse package to use.
By default, this option will use the `packages.default` as exposed by this flake.
'';
};
systemd = mkOption {
type = types.bool;
default = pkgs.stdenv.isLinux;
description = "Whether to enable to systemd service for lan-mouse.";
};
};
config = mkIf cfg.enable {
systemd.user.services.lan-mouse = lib.mkIf cfg.systemd {
Unit = {
Description = "Systemd service for Lan Mouse";
Requires = ["graphical-session.target"];
};
Service = {
Type = "simple";
ExecStart = "${cfg.package}/bin/lan-mouse --daemon";
};
Install.WantedBy = [
(lib.mkIf config.wayland.windowManager.hyprland.systemd.enable "hyprland-session.target")
(lib.mkIf config.wayland.windowManager.sway.systemd.enable "sway-session.target")
];
};
home.packages = [
cfg.package
];
};
}

View File

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -7,6 +7,6 @@
<file compressed="true">style-dark.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">mouse-icon.svg</file>
</gresource> </gresource>
</gresources> </gresources>

View File

@@ -35,7 +35,7 @@
<object class="AdwStatusPage"> <object class="AdwStatusPage">
<property name="title" translatable="yes">Lan Mouse</property> <property name="title" translatable="yes">Lan Mouse</property>
<property name="description" translatable="yes">easily use your mouse and keyboard on multiple computers</property> <property name="description" translatable="yes">easily use your mouse and keyboard on multiple computers</property>
<property name="icon-name">de.feschber.LanMouse</property> <property name="icon-name">mouse-icon</property>
<property name="child"> <property name="child">
<object class="AdwClamp"> <object class="AdwClamp">
<property name="maximum-size">600</property> <property name="maximum-size">600</property>

View File

@@ -1,13 +0,0 @@
[Unit]
Description=Lan Mouse
# lan mouse needs an active graphical session
After=graphical-session.target
# make sure the service terminates with the graphical session
BindsTo=graphical-session.target
[Service]
ExecStart=/usr/bin/lan-mouse --daemon
Restart=on-failure
[Install]
WantedBy=graphical-session.target

View File

@@ -1 +0,0 @@
(builtins.getFlake ("git+file://" + toString ./.)).devShells.${builtins.currentSystem}.default

2
src/backend.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod consumer;
pub mod producer;

20
src/backend/consumer.rs Normal file
View File

@@ -0,0 +1,20 @@
#[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 consumer
pub mod dummy;

View File

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

View File

@@ -1,17 +1,15 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
os::{fd::OwnedFd, unix::net::UnixStream}, io,
os::{
fd::{FromRawFd, RawFd},
unix::net::UnixStream,
},
time::{SystemTime, UNIX_EPOCH}, time::{SystemTime, UNIX_EPOCH},
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use ashpd::{ use ashpd::desktop::remote_desktop::{DeviceType, RemoteDesktop};
desktop::{
remote_desktop::{DeviceType, RemoteDesktop},
ResponseError,
},
WindowIdentifier,
};
use async_trait::async_trait; use async_trait::async_trait;
use futures::StreamExt; use futures::StreamExt;
@@ -23,11 +21,11 @@ use reis::{
use crate::{ use crate::{
client::{ClientEvent, ClientHandle}, client::{ClientEvent, ClientHandle},
emulate::InputEmulation, consumer::EventConsumer,
event::Event, event::Event,
}; };
pub struct LibeiEmulation { pub struct LibeiConsumer {
handshake: bool, handshake: bool,
context: ei::Context, context: ei::Context,
events: EiEventStream, events: EiEventStream,
@@ -45,42 +43,38 @@ pub struct LibeiEmulation {
serial: u32, serial: u32,
} }
async fn get_ei_fd() -> Result<OwnedFd, ashpd::Error> { async fn get_ei_fd() -> Result<RawFd, ashpd::Error> {
let proxy = RemoteDesktop::new().await?; let proxy = RemoteDesktop::new().await?;
let session = proxy.create_session().await?;
// retry when user presses the cancel button // I HATE EVERYTHING, THIS TOOK 8 HOURS OF DEBUGGING
let (session, _) = loop { proxy
log::debug!("creating session ..."); .select_devices(
let session = proxy.create_session().await?; &session,
DeviceType::Pointer | DeviceType::Keyboard | DeviceType::Touchscreen,
log::debug!("selecting devices ..."); )
proxy .await?;
.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
.start(&session, &ashpd::WindowIdentifier::default())
.await?
.response()?;
proxy.connect_to_eis(&session).await proxy.connect_to_eis(&session).await
} }
impl LibeiEmulation { impl LibeiConsumer {
pub async fn new() -> Result<Self> { pub async fn new() -> Result<Self> {
// fd is owned by the message, so we need to dup it // fd is owned by the message, so we need to dup it
let eifd = get_ei_fd().await?; let eifd = get_ei_fd().await?;
let stream = UnixStream::from(eifd); let eifd = unsafe {
let ret = libc::dup(eifd);
if ret < 0 {
Err(io::Error::last_os_error())
} else {
Ok(ret)
}
}?;
let stream = unsafe { UnixStream::from_raw_fd(eifd) };
// let stream = UnixStream::connect("/run/user/1000/eis-0")?; // let stream = UnixStream::connect("/run/user/1000/eis-0")?;
stream.set_nonblocking(true)?; stream.set_nonblocking(true)?;
let context = ei::Context::new(stream)?; let context = ei::Context::new(stream)?;
@@ -107,7 +101,7 @@ impl LibeiEmulation {
} }
#[async_trait] #[async_trait]
impl InputEmulation for LibeiEmulation { impl EventConsumer for LibeiConsumer {
async fn consume(&mut self, event: Event, _client_handle: ClientHandle) { async fn consume(&mut self, event: Event, _client_handle: ClientHandle) {
let now = SystemTime::now() let now = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)

View File

@@ -1,24 +1,17 @@
use crate::client::{ClientEvent, ClientHandle}; use crate::client::{ClientEvent, ClientHandle};
use crate::emulate::InputEmulation; use crate::consumer::EventConsumer;
use crate::event::{Event, KeyboardEvent, PointerEvent}; use crate::event::{Event, KeyboardEvent, PointerEvent};
use anyhow::{anyhow, Result}; 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, CGMouseButton, EventField, ScrollEventUnit,
}; };
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID}; use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
use keycode::{KeyMap, KeyMapping};
use std::ops::{Index, IndexMut}; use std::ops::{Index, IndexMut};
use std::time::Duration;
use tokio::task::AbortHandle;
const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500); pub struct MacOSConsumer {
const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32);
pub struct MacOSEmulation {
pub event_source: CGEventSource, pub event_source: CGEventSource,
repeat_task: Option<AbortHandle>,
button_state: ButtonState, button_state: ButtonState,
} }
@@ -50,9 +43,9 @@ impl IndexMut<CGMouseButton> for ButtonState {
} }
} }
unsafe impl Send for MacOSEmulation {} unsafe impl Send for MacOSConsumer {}
impl MacOSEmulation { impl MacOSConsumer {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
let event_source = match CGEventSource::new(CGEventSourceStateID::CombinedSessionState) { let event_source = match CGEventSource::new(CGEventSourceStateID::CombinedSessionState) {
Ok(e) => e, Ok(e) => e,
@@ -66,7 +59,6 @@ impl MacOSEmulation {
Ok(Self { Ok(Self {
event_source, event_source,
button_state, button_state,
repeat_task: None,
}) })
} }
@@ -74,41 +66,10 @@ impl MacOSEmulation {
let event: CGEvent = CGEvent::new(self.event_source.clone()).ok()?; let event: CGEvent = CGEvent::new(self.event_source.clone()).ok()?;
Some(event.location()) Some(event.location())
} }
async fn spawn_repeat_task(&mut self, key: u16) {
// there can only be one repeating key and it's
// always the last to be pressed
self.kill_repeat_task();
let event_source = self.event_source.clone();
let repeat_task = tokio::task::spawn_local(async move {
tokio::time::sleep(DEFAULT_REPEAT_DELAY).await;
loop {
key_event(event_source.clone(), key, 1);
tokio::time::sleep(DEFAULT_REPEAT_INTERVAL).await;
}
});
self.repeat_task = Some(repeat_task.abort_handle());
}
fn kill_repeat_task(&mut self) {
if let Some(task) = self.repeat_task.take() {
task.abort();
}
}
}
fn key_event(event_source: CGEventSource, key: u16, state: u8) {
let event = match CGEvent::new_keyboard_event(event_source, key, state != 0) {
Ok(e) => e,
Err(_) => {
log::warn!("unable to create key event");
return;
}
};
event.post(CGEventTapLocation::HID);
} }
#[async_trait] #[async_trait]
impl InputEmulation for MacOSEmulation { impl EventConsumer for MacOSConsumer {
async fn consume(&mut self, event: Event, _client_handle: ClientHandle) { async fn consume(&mut self, event: Event, _client_handle: ClientHandle) {
match event { match event {
Event::Pointer(pointer_event) => match pointer_event { Event::Pointer(pointer_event) => match pointer_event {
@@ -248,24 +209,22 @@ impl InputEmulation for MacOSEmulation {
PointerEvent::Frame { .. } => {} PointerEvent::Frame { .. } => {}
}, },
Event::Keyboard(keyboard_event) => match keyboard_event { Event::Keyboard(keyboard_event) => match keyboard_event {
KeyboardEvent::Key { KeyboardEvent::Key { .. } => {
time: _, /*
key, let code = CGKeyCode::from_le(key as u16);
state, let event = match CGEvent::new_keyboard_event(
} => { self.event_source.clone(),
let code = match KeyMap::from_key_mapping(KeyMapping::Evdev(key as u16)) { code,
Ok(k) => k.mac as CGKeyCode, match state { 1 => true, _ => false }
) {
Ok(e) => e,
Err(_) => { Err(_) => {
log::warn!("unable to map key event"); log::warn!("unable to create key event");
return; return
} }
}; };
match state { event.post(CGEventTapLocation::HID);
// pressed */
1 => self.spawn_repeat_task(code).await,
_ => self.kill_repeat_task(),
}
key_event(self.event_source.clone(), code, state)
} }
KeyboardEvent::Modifiers { .. } => {} KeyboardEvent::Modifiers { .. } => {}
}, },

View File

@@ -1,19 +1,19 @@
use crate::{ use crate::{
emulate::InputEmulation, consumer::EventConsumer,
event::{KeyboardEvent, PointerEvent}, event::{KeyboardEvent, PointerEvent},
scancode, scancode,
}; };
use anyhow::Result; use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use std::ops::BitOrAssign; use winapi::um::winuser::{SendInput, KEYEVENTF_EXTENDEDKEY};
use std::time::Duration; use winapi::{
use tokio::task::AbortHandle; self,
use windows::Win32::UI::Input::KeyboardAndMouse::{SendInput, INPUT_0, KEYEVENTF_EXTENDEDKEY}; um::winuser::{
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, LPINPUT, MOUSEEVENTF_HWHEEL, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP,
MOUSEEVENTF_HWHEEL, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN,
MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP, MOUSEEVENTF_RIGHTUP, MOUSEEVENTF_WHEEL, MOUSEINPUT,
MOUSEEVENTF_WHEEL, MOUSEINPUT, },
}; };
use crate::{ use crate::{
@@ -21,21 +21,16 @@ use crate::{
event::Event, event::Event,
}; };
const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500); pub struct WindowsConsumer {}
const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32);
pub struct WindowsEmulation { impl WindowsConsumer {
repeat_task: Option<AbortHandle>,
}
impl WindowsEmulation {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
Ok(Self { repeat_task: None }) Ok(Self {})
} }
} }
#[async_trait] #[async_trait]
impl InputEmulation for WindowsEmulation { impl EventConsumer for WindowsConsumer {
async fn consume(&mut self, event: Event, _: ClientHandle) { async fn consume(&mut self, event: Event, _: ClientHandle) {
match event { match event {
Event::Pointer(pointer_event) => match pointer_event { Event::Pointer(pointer_event) => match pointer_event {
@@ -63,15 +58,7 @@ impl InputEmulation for WindowsEmulation {
time: _, time: _,
key, key,
state, state,
} => { } => key_event(key, state),
match state {
// pressed
0 => self.kill_repeat_task(),
1 => self.spawn_repeat_task(key).await,
_ => {}
}
key_event(key, state)
}
KeyboardEvent::Modifiers { .. } => {} KeyboardEvent::Modifiers { .. } => {}
}, },
_ => {} _ => {}
@@ -85,51 +72,21 @@ impl InputEmulation for WindowsEmulation {
async fn destroy(&mut self) {} async fn destroy(&mut self) {}
} }
impl WindowsEmulation {
async fn spawn_repeat_task(&mut self, key: u32) {
// there can only be one repeating key and it's
// always the last to be pressed
self.kill_repeat_task();
let repeat_task = tokio::task::spawn_local(async move {
tokio::time::sleep(DEFAULT_REPEAT_DELAY).await;
loop {
key_event(key, 1);
tokio::time::sleep(DEFAULT_REPEAT_INTERVAL).await;
}
});
self.repeat_task = Some(repeat_task.abort_handle());
}
fn kill_repeat_task(&mut self) {
if let Some(task) = self.repeat_task.take() {
task.abort();
}
}
}
fn send_input_safe(input: INPUT) {
unsafe {
loop {
/* retval = number of successfully submitted events */
if SendInput(&[input], std::mem::size_of::<INPUT>() as i32) > 0 {
break;
}
}
}
}
fn send_mouse_input(mi: MOUSEINPUT) { fn send_mouse_input(mi: MOUSEINPUT) {
send_input_safe(INPUT { unsafe {
r#type: INPUT_MOUSE, let mut input = INPUT {
Anonymous: INPUT_0 { mi }, type_: INPUT_MOUSE,
}); u: std::mem::transmute(mi),
};
SendInput(
1_u32,
&mut input as LPINPUT,
std::mem::size_of::<INPUT>() as i32,
);
}
} }
fn send_keyboard_input(ki: KEYBDINPUT) {
send_input_safe(INPUT {
r#type: INPUT_KEYBOARD,
Anonymous: INPUT_0 { ki },
});
}
fn rel_mouse(dx: i32, dy: i32) { fn rel_mouse(dx: i32, dy: i32) {
let mi = MOUSEINPUT { let mi = MOUSEINPUT {
dx, dx,
@@ -193,23 +150,33 @@ fn key_event(key: u32, state: u8) {
}; };
let extended = scancode > 0xff; let extended = scancode > 0xff;
let scancode = scancode & 0xff; let scancode = scancode & 0xff;
let mut flags = KEYEVENTF_SCANCODE;
if extended {
flags.bitor_assign(KEYEVENTF_EXTENDEDKEY);
}
if state == 0 {
flags.bitor_assign(KEYEVENTF_KEYUP);
}
let ki = KEYBDINPUT { let ki = KEYBDINPUT {
wVk: Default::default(), wVk: 0,
wScan: scancode, wScan: scancode,
dwFlags: flags, dwFlags: KEYEVENTF_SCANCODE
| if extended { KEYEVENTF_EXTENDEDKEY } else { 0 }
| match state {
0 => KEYEVENTF_KEYUP,
1 => 0u32,
_ => return,
},
time: 0, time: 0,
dwExtraInfo: 0, dwExtraInfo: 0,
}; };
send_keyboard_input(ki); send_keyboard_input(ki);
} }
fn send_keyboard_input(ki: KEYBDINPUT) {
unsafe {
let mut input = INPUT {
type_: INPUT_KEYBOARD,
u: std::mem::zeroed(),
};
*input.u.ki_mut() = ki;
SendInput(1_u32, &mut input, std::mem::size_of::<INPUT>() as i32);
}
}
fn linux_keycode_to_windows_scancode(linux_keycode: u32) -> Option<u16> { fn linux_keycode_to_windows_scancode(linux_keycode: u32) -> Option<u16> {
let linux_scancode = match scancode::Linux::try_from(linux_keycode) { let linux_scancode = match scancode::Linux::try_from(linux_keycode) {
Ok(s) => s, Ok(s) => s,

View File

@@ -1,5 +1,5 @@
use crate::client::{ClientEvent, ClientHandle}; use crate::client::{ClientEvent, ClientHandle};
use crate::emulate::InputEmulation; use crate::consumer::EventConsumer;
use async_trait::async_trait; use async_trait::async_trait;
use std::collections::HashMap; use std::collections::HashMap;
use std::io; use std::io;
@@ -40,13 +40,13 @@ struct State {
} }
// App State, implements Dispatch event handlers // App State, implements Dispatch event handlers
pub(crate) struct WlrootsEmulation { pub(crate) struct WlrootsConsumer {
last_flush_failed: bool, last_flush_failed: bool,
state: State, state: State,
queue: EventQueue<State>, queue: EventQueue<State>,
} }
impl WlrootsEmulation { impl WlrootsConsumer {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
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)?;
@@ -62,7 +62,7 @@ impl WlrootsEmulation {
let input_for_client: HashMap<ClientHandle, VirtualInput> = HashMap::new(); let input_for_client: HashMap<ClientHandle, VirtualInput> = HashMap::new();
let mut emulate = WlrootsEmulation { let mut consumer = WlrootsConsumer {
last_flush_failed: false, last_flush_failed: false,
state: State { state: State {
keymap: None, keymap: None,
@@ -74,13 +74,16 @@ impl WlrootsEmulation {
}, },
queue, queue,
}; };
while emulate.state.keymap.is_none() { while consumer.state.keymap.is_none() {
emulate.queue.blocking_dispatch(&mut emulate.state).unwrap(); consumer
.queue
.blocking_dispatch(&mut consumer.state)
.unwrap();
} }
// let fd = unsafe { &File::from_raw_fd(emulate.state.keymap.unwrap().1.as_raw_fd()) }; // let fd = unsafe { &File::from_raw_fd(consumer.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() };
// log::debug!("{:?}", &mmap[..100]); // log::debug!("{:?}", &mmap[..100]);
Ok(emulate) Ok(consumer)
} }
} }
@@ -103,7 +106,7 @@ impl State {
} }
#[async_trait] #[async_trait]
impl InputEmulation for WlrootsEmulation { impl EventConsumer for WlrootsConsumer {
async fn consume(&mut self, event: Event, client_handle: ClientHandle) { async fn consume(&mut self, event: Event, client_handle: ClientHandle) {
if let Some(virtual_input) = self.state.input_for_client.get(&client_handle) { if let Some(virtual_input) = self.state.input_for_client.get(&client_handle) {
if self.last_flush_failed { if self.last_flush_failed {

View File

@@ -2,28 +2,28 @@ use anyhow::{anyhow, Result};
use async_trait::async_trait; use async_trait::async_trait;
use std::ptr; use std::ptr;
use x11::{ use x11::{
xlib::{self, XCloseDisplay}, xlib::{self, XCloseDisplay, XOpenDisplay},
xtest, xtest,
}; };
use crate::{ use crate::{
client::ClientHandle, client::ClientHandle,
emulate::InputEmulation, consumer::EventConsumer,
event::{ event::{
Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT,
}, },
}; };
pub struct X11Emulation { pub struct X11Consumer {
display: *mut xlib::Display, display: *mut xlib::Display,
} }
unsafe impl Send for X11Emulation {} unsafe impl Send for X11Consumer {}
impl X11Emulation { impl X11Consumer {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
let display = unsafe { let display = unsafe {
match xlib::XOpenDisplay(ptr::null()) { match 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(anyhow!("could not open display"))
} }
@@ -91,7 +91,7 @@ impl X11Emulation {
} }
} }
impl Drop for X11Emulation { impl Drop for X11Consumer {
fn drop(&mut self) { fn drop(&mut self) {
unsafe { unsafe {
XCloseDisplay(self.display); XCloseDisplay(self.display);
@@ -100,7 +100,7 @@ impl Drop for X11Emulation {
} }
#[async_trait] #[async_trait]
impl InputEmulation for X11Emulation { impl EventConsumer for X11Consumer {
async fn consume(&mut self, event: Event, _: ClientHandle) { async fn consume(&mut self, event: Event, _: ClientHandle) {
match event { match event {
Event::Pointer(pointer_event) => match pointer_event { Event::Pointer(pointer_event) => match pointer_event {

View File

@@ -2,7 +2,7 @@ use anyhow::Result;
use ashpd::{ use ashpd::{
desktop::{ desktop::{
remote_desktop::{Axis, DeviceType, KeyState, RemoteDesktop}, remote_desktop::{Axis, DeviceType, KeyState, RemoteDesktop},
ResponseError, Session, Session,
}, },
WindowIdentifier, WindowIdentifier,
}; };
@@ -10,48 +10,33 @@ use async_trait::async_trait;
use crate::{ use crate::{
client::ClientEvent, client::ClientEvent,
emulate::InputEmulation, consumer::EventConsumer,
event::{ event::{
Event::{Keyboard, Pointer}, Event::{Keyboard, Pointer},
KeyboardEvent, PointerEvent, KeyboardEvent, PointerEvent,
}, },
}; };
pub struct DesktopPortalEmulation<'a> { pub struct DesktopPortalConsumer<'a> {
proxy: RemoteDesktop<'a>, proxy: RemoteDesktop<'a>,
session: Session<'a>, session: Session<'a>,
} }
impl<'a> DesktopPortalEmulation<'a> { impl<'a> DesktopPortalConsumer<'a> {
pub async fn new() -> Result<DesktopPortalEmulation<'a>> { pub async fn new() -> Result<DesktopPortalConsumer<'a>> {
log::debug!("connecting to org.freedesktop.portal.RemoteDesktop portal ..."); log::debug!("connecting to org.freedesktop.portal.RemoteDesktop portal ...");
let proxy = RemoteDesktop::new().await?; let proxy = RemoteDesktop::new().await?;
log::debug!("creating session ...");
let session = proxy.create_session().await?;
log::debug!("selecting devices ...");
proxy
.select_devices(&session, DeviceType::Keyboard | DeviceType::Pointer)
.await?;
// retry when user presses the cancel button let _ = proxy
let (session, _) = loop { .start(&session, &WindowIdentifier::default())
log::debug!("creating session ..."); .await?
let session = proxy.create_session().await?; .response()?;
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"); log::debug!("started session");
Ok(Self { proxy, session }) Ok(Self { proxy, session })
@@ -59,7 +44,7 @@ impl<'a> DesktopPortalEmulation<'a> {
} }
#[async_trait] #[async_trait]
impl<'a> InputEmulation for DesktopPortalEmulation<'a> { impl<'a> EventConsumer for DesktopPortalConsumer<'a> {
async fn consume(&mut self, event: crate::event::Event, _client: crate::client::ClientHandle) { async fn consume(&mut self, event: crate::event::Event, _client: crate::client::ClientHandle) {
match event { match event {
Pointer(p) => { Pointer(p) => {

17
src/backend/producer.rs Normal file
View File

@@ -0,0 +1,17 @@
#[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 event producer
pub mod dummy;

View File

@@ -4,26 +4,26 @@ use std::task::{Context, Poll};
use futures_core::Stream; use futures_core::Stream;
use crate::capture::InputCapture;
use crate::event::Event; use crate::event::Event;
use crate::producer::EventProducer;
use crate::client::{ClientEvent, ClientHandle}; use crate::client::{ClientEvent, ClientHandle};
pub struct DummyInputCapture {} pub struct DummyProducer {}
impl DummyInputCapture { impl DummyProducer {
pub fn new() -> Self { pub fn new() -> Self {
Self {} Self {}
} }
} }
impl Default for DummyInputCapture { impl Default for DummyProducer {
fn default() -> Self { fn default() -> Self {
Self::new() Self::new()
} }
} }
impl InputCapture for DummyInputCapture { impl EventProducer for DummyProducer {
fn notify(&mut self, _event: ClientEvent) -> io::Result<()> { fn notify(&mut self, _event: ClientEvent) -> io::Result<()> {
Ok(()) Ok(())
} }
@@ -33,7 +33,7 @@ impl InputCapture for DummyInputCapture {
} }
} }
impl Stream for DummyInputCapture { impl Stream for DummyProducer {
type Item = io::Result<(ClientHandle, Event)>; type Item = io::Result<(ClientHandle, 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>> {

View File

@@ -1,23 +1,23 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use std::io; use std::{io, task::Poll};
use std::task::Poll;
use futures_core::Stream; use futures_core::Stream;
use crate::capture::InputCapture; use crate::{
use crate::event::Event; client::{ClientEvent, ClientHandle},
event::Event,
producer::EventProducer,
};
use crate::client::{ClientEvent, ClientHandle}; pub struct LibeiProducer {}
pub struct X11InputCapture {} impl LibeiProducer {
impl X11InputCapture {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
Err(anyhow!("not implemented")) Err(anyhow!("not implemented"))
} }
} }
impl InputCapture for X11InputCapture { impl EventProducer for LibeiProducer {
fn notify(&mut self, _event: ClientEvent) -> io::Result<()> { fn notify(&mut self, _event: ClientEvent) -> io::Result<()> {
Ok(()) Ok(())
} }
@@ -27,7 +27,7 @@ impl InputCapture for X11InputCapture {
} }
} }
impl Stream for X11InputCapture { impl Stream for LibeiProducer {
type Item = io::Result<(ClientHandle, Event)>; type Item = io::Result<(ClientHandle, Event)>;
fn poll_next( fn poll_next(

View File

@@ -1,20 +1,20 @@
use crate::capture::InputCapture;
use crate::client::{ClientEvent, ClientHandle}; use crate::client::{ClientEvent, ClientHandle};
use crate::event::Event; use crate::event::Event;
use crate::producer::EventProducer;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use futures_core::Stream; use futures_core::Stream;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use std::{io, pin::Pin}; use std::{io, pin::Pin};
pub struct MacOSInputCapture; pub struct MacOSProducer;
impl MacOSInputCapture { impl MacOSProducer {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
Err(anyhow!("not yet implemented")) Err(anyhow!("not yet implemented"))
} }
} }
impl Stream for MacOSInputCapture { impl Stream for MacOSProducer {
type Item = io::Result<(ClientHandle, Event)>; type Item = io::Result<(ClientHandle, 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>> {
@@ -22,7 +22,7 @@ impl Stream for MacOSInputCapture {
} }
} }
impl InputCapture for MacOSInputCapture { impl EventProducer for MacOSProducer {
fn notify(&mut self, _event: ClientEvent) -> io::Result<()> { fn notify(&mut self, _event: ClientEvent) -> io::Result<()> {
Ok(()) Ok(())
} }

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
capture::InputCapture,
client::{ClientEvent, ClientHandle, Position}, client::{ClientEvent, ClientHandle, Position},
producer::EventProducer,
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
@@ -54,12 +54,9 @@ use wayland_client::{
delegate_noop, delegate_noop,
globals::{registry_queue_init, GlobalListContents}, globals::{registry_queue_init, GlobalListContents},
protocol::{ protocol::{
wl_buffer, wl_compositor, wl_buffer, wl_compositor, wl_keyboard,
wl_keyboard::{self, WlKeyboard},
wl_output::{self, WlOutput}, wl_output::{self, WlOutput},
wl_pointer::{self, WlPointer}, wl_pointer, wl_region, wl_registry, wl_seat, wl_shm, wl_shm_pool, wl_surface,
wl_region, wl_registry, wl_seat, wl_shm, wl_shm_pool,
wl_surface::WlSurface,
}, },
Connection, Dispatch, DispatchError, EventQueue, QueueHandle, WEnum, Connection, Dispatch, DispatchError, EventQueue, QueueHandle, WEnum,
}; };
@@ -76,7 +73,7 @@ struct Globals {
seat: wl_seat::WlSeat, seat: wl_seat::WlSeat,
shm: wl_shm::WlShm, shm: wl_shm::WlShm,
layer_shell: ZwlrLayerShellV1, layer_shell: ZwlrLayerShellV1,
outputs: Vec<WlOutput>, outputs: Vec<wl_output::WlOutput>,
xdg_output_manager: ZxdgOutputManagerV1, xdg_output_manager: ZxdgOutputManagerV1,
} }
@@ -98,8 +95,6 @@ impl OutputInfo {
} }
struct State { struct State {
pointer: Option<WlPointer>,
keyboard: Option<WlKeyboard>,
pointer_lock: Option<ZwpLockedPointerV1>, pointer_lock: Option<ZwpLockedPointerV1>,
rel_pointer: Option<ZwpRelativePointerV1>, rel_pointer: Option<ZwpRelativePointerV1>,
shortcut_inhibitor: Option<ZwpKeyboardShortcutsInhibitorV1>, shortcut_inhibitor: Option<ZwpKeyboardShortcutsInhibitorV1>,
@@ -124,11 +119,11 @@ impl AsRawFd for Inner {
} }
} }
pub struct WaylandInputCapture(AsyncFd<Inner>); pub struct WaylandEventProducer(AsyncFd<Inner>);
struct Window { struct Window {
buffer: wl_buffer::WlBuffer, buffer: wl_buffer::WlBuffer,
surface: WlSurface, surface: wl_surface::WlSurface,
layer_surface: ZwlrLayerSurfaceV1, layer_surface: ZwlrLayerSurfaceV1,
pos: Position, pos: Position,
} }
@@ -141,7 +136,6 @@ impl Window {
pos: Position, pos: Position,
size: (i32, i32), size: (i32, i32),
) -> Window { ) -> Window {
log::debug!("creating window output: {output:?}, size: {size:?}");
let g = &state.g; let g = &state.g;
let (width, height) = match pos { let (width, height) = match pos {
@@ -223,7 +217,6 @@ fn get_edges(outputs: &[(WlOutput, OutputInfo)], pos: Position) -> Vec<(WlOutput
fn get_output_configuration(state: &State, pos: Position) -> Vec<(WlOutput, OutputInfo)> { fn get_output_configuration(state: &State, pos: Position) -> Vec<(WlOutput, OutputInfo)> {
// get all output edges corresponding to the position // get all output edges corresponding to the position
let edges = get_edges(&state.output_info, pos); let edges = get_edges(&state.output_info, pos);
log::debug!("edges: {edges:?}");
let opposite_edges = get_edges(&state.output_info, pos.opposite()); let opposite_edges = get_edges(&state.output_info, pos.opposite());
// remove those edges that are at the same position // remove those edges that are at the same position
@@ -256,7 +249,7 @@ fn draw(f: &mut File, (width, height): (u32, u32)) {
} }
} }
impl WaylandInputCapture { impl WaylandEventProducer {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
let conn = match Connection::connect_to_env() { let conn = match Connection::connect_to_env() {
Ok(c) => c, Ok(c) => c,
@@ -338,8 +331,6 @@ impl WaylandInputCapture {
std::mem::drop(read_guard); std::mem::drop(read_guard);
let mut state = State { let mut state = State {
pointer: None,
keyboard: None,
g, g,
pointer_lock: None, pointer_lock: None,
rel_pointer: None, rel_pointer: None,
@@ -390,7 +381,7 @@ impl WaylandInputCapture {
let inner = AsyncFd::new(Inner { queue, state })?; let inner = AsyncFd::new(Inner { queue, state })?;
Ok(WaylandInputCapture(inner)) Ok(WaylandEventProducer(inner))
} }
fn add_client(&mut self, handle: ClientHandle, pos: Position) { fn add_client(&mut self, handle: ClientHandle, pos: Position) {
@@ -415,8 +406,8 @@ impl WaylandInputCapture {
impl State { impl State {
fn grab( fn grab(
&mut self, &mut self,
surface: &WlSurface, surface: &wl_surface::WlSurface,
pointer: &WlPointer, pointer: &wl_pointer::WlPointer,
serial: u32, serial: u32,
qh: &QueueHandle<State>, qh: &QueueHandle<State>,
) { ) {
@@ -498,7 +489,6 @@ impl State {
fn add_client(&mut self, client: ClientHandle, pos: Position) { fn add_client(&mut self, client: ClientHandle, pos: Position) {
let outputs = get_output_configuration(self, pos); let outputs = get_output_configuration(self, pos);
log::debug!("outputs: {outputs:?}");
outputs.iter().for_each(|(o, i)| { outputs.iter().for_each(|(o, i)| {
let window = Window::new(self, &self.qh, o, pos, i.size); let window = Window::new(self, &self.qh, o, pos, i.size);
let window = Rc::new(window); let window = Rc::new(window);
@@ -587,7 +577,7 @@ impl Inner {
} }
} }
impl InputCapture for WaylandInputCapture { impl EventProducer for WaylandEventProducer {
fn notify(&mut self, client_event: ClientEvent) -> io::Result<()> { fn notify(&mut self, client_event: ClientEvent) -> io::Result<()> {
match client_event { match client_event {
ClientEvent::Create(handle, pos) => { ClientEvent::Create(handle, pos) => {
@@ -609,7 +599,7 @@ impl InputCapture for WaylandInputCapture {
} }
} }
impl Stream for WaylandInputCapture { impl Stream for WaylandEventProducer {
type Item = io::Result<(ClientHandle, Event)>; type Item = io::Result<(ClientHandle, 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>> {
@@ -664,7 +654,7 @@ impl Stream for WaylandInputCapture {
impl Dispatch<wl_seat::WlSeat, ()> for State { impl Dispatch<wl_seat::WlSeat, ()> for State {
fn event( fn event(
state: &mut Self, _: &mut Self,
seat: &wl_seat::WlSeat, seat: &wl_seat::WlSeat,
event: <wl_seat::WlSeat as wayland_client::Proxy>::Event, event: <wl_seat::WlSeat as wayland_client::Proxy>::Event,
_: &(), _: &(),
@@ -675,21 +665,21 @@ impl Dispatch<wl_seat::WlSeat, ()> for State {
capabilities: WEnum::Value(capabilities), capabilities: WEnum::Value(capabilities),
} = event } = event
{ {
if capabilities.contains(wl_seat::Capability::Pointer) && state.pointer.is_none() { if capabilities.contains(wl_seat::Capability::Pointer) {
state.pointer.replace(seat.get_pointer(qh, ())); seat.get_pointer(qh, ());
} }
if capabilities.contains(wl_seat::Capability::Keyboard) && state.keyboard.is_none() { if capabilities.contains(wl_seat::Capability::Keyboard) {
seat.get_keyboard(qh, ()); seat.get_keyboard(qh, ());
} }
} }
} }
} }
impl Dispatch<WlPointer, ()> for State { impl Dispatch<wl_pointer::WlPointer, ()> for State {
fn event( fn event(
app: &mut Self, app: &mut Self,
pointer: &WlPointer, pointer: &wl_pointer::WlPointer,
event: <WlPointer as wayland_client::Proxy>::Event, event: <wl_pointer::WlPointer as wayland_client::Proxy>::Event,
_: &(), _: &(),
_: &Connection, _: &Connection,
qh: &QueueHandle<Self>, qh: &QueueHandle<Self>,
@@ -722,16 +712,6 @@ impl Dispatch<WlPointer, ()> for State {
app.pending_events.push_back((*client, Event::Enter())); app.pending_events.push_back((*client, Event::Enter()));
} }
wl_pointer::Event::Leave { .. } => { wl_pointer::Event::Leave { .. } => {
/* There are rare cases, where when a window is opened in
* just the wrong moment, the pointer is released, while
* still grabbed.
* In that case, the pointer must be ungrabbed, otherwise
* it is impossible to grab it again (since the pointer
* lock, relative pointer,... objects are still in place)
*/
if app.pointer_lock.is_some() {
log::warn!("compositor released mouse");
}
app.ungrab(); app.ungrab();
} }
wl_pointer::Event::Button { wl_pointer::Event::Button {
@@ -771,10 +751,10 @@ impl Dispatch<WlPointer, ()> for State {
} }
} }
impl Dispatch<WlKeyboard, ()> for State { impl Dispatch<wl_keyboard::WlKeyboard, ()> for State {
fn event( fn event(
app: &mut Self, app: &mut Self,
_: &WlKeyboard, _: &wl_keyboard::WlKeyboard,
event: wl_keyboard::Event, event: wl_keyboard::Event,
_: &(), _: &(),
_: &Connection, _: &Connection,
@@ -927,7 +907,7 @@ impl Dispatch<wl_registry::WlRegistry, ()> for State {
state state
.g .g
.outputs .outputs
.push(registry.bind::<WlOutput, _, _>(name, 4, qh, ())) .push(registry.bind::<wl_output::WlOutput, _, _>(name, 4, qh, ()))
} }
} }
wl_registry::Event::GlobalRemove { .. } => {} wl_registry::Event::GlobalRemove { .. } => {}
@@ -972,11 +952,11 @@ impl Dispatch<ZxdgOutputV1, WlOutput> for State {
} }
} }
impl Dispatch<WlOutput, ()> for State { impl Dispatch<wl_output::WlOutput, ()> for State {
fn event( fn event(
state: &mut Self, state: &mut Self,
_proxy: &WlOutput, _proxy: &wl_output::WlOutput,
event: <WlOutput as wayland_client::Proxy>::Event, event: <wl_output::WlOutput as wayland_client::Proxy>::Event,
_data: &(), _data: &(),
_conn: &Connection, _conn: &Connection,
_qhandle: &QueueHandle<Self>, _qhandle: &QueueHandle<Self>,
@@ -1000,6 +980,6 @@ delegate_noop!(State: ZwpPointerConstraintsV1);
delegate_noop!(State: ignore ZxdgOutputManagerV1); delegate_noop!(State: ignore ZxdgOutputManagerV1);
delegate_noop!(State: ignore wl_shm::WlShm); delegate_noop!(State: ignore wl_shm::WlShm);
delegate_noop!(State: ignore wl_buffer::WlBuffer); delegate_noop!(State: ignore wl_buffer::WlBuffer);
delegate_noop!(State: ignore WlSurface); delegate_noop!(State: ignore wl_surface::WlSurface);
delegate_noop!(State: ignore ZwpKeyboardShortcutsInhibitorV1); delegate_noop!(State: ignore ZwpKeyboardShortcutsInhibitorV1);
delegate_noop!(State: ignore ZwpLockedPointerV1); delegate_noop!(State: ignore ZwpLockedPointerV1);

View File

@@ -4,14 +4,14 @@ use futures::Stream;
use std::{io, pin::Pin}; use std::{io, pin::Pin};
use crate::{ use crate::{
capture::InputCapture,
client::{ClientEvent, ClientHandle}, client::{ClientEvent, ClientHandle},
event::Event, event::Event,
producer::EventProducer,
}; };
pub struct WindowsInputCapture {} pub struct WindowsProducer {}
impl InputCapture for WindowsInputCapture { impl EventProducer for WindowsProducer {
fn notify(&mut self, _event: ClientEvent) -> io::Result<()> { fn notify(&mut self, _event: ClientEvent) -> io::Result<()> {
Ok(()) Ok(())
} }
@@ -21,13 +21,13 @@ impl InputCapture for WindowsInputCapture {
} }
} }
impl WindowsInputCapture { impl WindowsProducer {
pub(crate) fn new() -> Result<Self> { pub(crate) fn new() -> Result<Self> {
Err(anyhow!("not implemented")) Err(anyhow!("not implemented"))
} }
} }
impl Stream for WindowsInputCapture { impl Stream for WindowsProducer {
type Item = io::Result<(ClientHandle, Event)>; type Item = io::Result<(ClientHandle, 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

255
src/backend/producer/x11.rs Normal file
View File

@@ -0,0 +1,255 @@
use anyhow::{anyhow, Result};
use x11::xinput::XGrabDevice;
use x11::xinput2::XIAllDevices;
use std::collections::VecDeque;
use std::os::fd::{AsRawFd, RawFd};
use std::task::{ready, Poll};
use std::{io, ptr};
use futures_core::Stream;
use crate::event::{Event, PointerEvent};
use crate::producer::EventProducer;
use crate::client::{ClientEvent, ClientHandle};
use tokio::io::unix::AsyncFd;
use x11::xlib::{
self, ButtonPress, ButtonPressMask, ButtonRelease, ButtonReleaseMask, CWBackPixel, CWEventMask,
CWOverrideRedirect, CopyFromParent, CurrentTime, EnterNotify, EnterWindowMask, ExposureMask,
GrabModeAsync, KeyPress, KeyPressMask, KeyRelease, KeyReleaseMask, LeaveWindowMask,
MotionNotify, PointerMotionMask, VisibilityChangeMask, XClassHint, XCloseDisplay,
XCreateWindow, XDefaultScreen, XFlush, XGetInputFocus, XGrabKeyboard,
XGrabPointer, XMapRaised, XNextEvent, XOpenDisplay, XPending, XRootWindow, XSetClassHint,
XSetWindowAttributes, XWhitePixel, XDefaultRootWindow,
};
pub struct X11Producer(AsyncFd<Inner>);
struct Inner {
connection_fd: RawFd,
display: *mut xlib::Display,
pending_events: VecDeque<(ClientHandle, Event)>,
window: u64,
}
impl AsRawFd for Inner {
fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd {
self.connection_fd
}
}
impl X11Producer {
pub fn new() -> Result<Self> {
let display = unsafe {
match XOpenDisplay(ptr::null()) {
d if d == ptr::null::<xlib::Display>() as *mut xlib::Display => {
Err(anyhow!("could not open display"))
}
display => Ok(display),
}
}?;
let screen = unsafe { XDefaultScreen(display) };
log::warn!("screen: {screen}");
let root_window = unsafe { XRootWindow(display, screen) };
log::warn!("root: {root_window}");
let mut attr: XSetWindowAttributes = unsafe { std::mem::zeroed() };
attr.override_redirect = true as i32;
attr.background_pixel = unsafe { XWhitePixel(display, screen) };
attr.event_mask = ExposureMask
| VisibilityChangeMask
| KeyPressMask
| KeyReleaseMask
| PointerMotionMask
| ButtonPressMask
| ButtonReleaseMask
| EnterWindowMask
| LeaveWindowMask;
let window = unsafe {
XCreateWindow(
display,
root_window,
0, /* x */
0, /* y */
2560, /* min width */
10, /* min height */
0, /* border width */
CopyFromParent, /* depth */
CopyFromParent as u32, /* class */
ptr::null_mut(), /* Visual *visual */
CWOverrideRedirect | CWBackPixel | CWEventMask,
&mut attr as *mut _,
)
};
let mut name: String = "lan-mouse".into();
let name = name.as_mut_ptr();
let mut class_hint = XClassHint {
res_name: name as *mut i8,
res_class: name as *mut i8,
};
unsafe { XSetClassHint(display, window, &mut class_hint as *mut _) };
log::warn!("window: {window}");
// unsafe { XSelectInput(display, window, event_mask as i64) };
unsafe { XMapRaised(display, window) };
unsafe { XFlush(display) };
/* can not fail */
let connection_fd = unsafe { xlib::XConnectionNumber(display) };
let pending_events = VecDeque::new();
let inner = Inner {
connection_fd,
display,
window,
pending_events,
};
let async_fd = AsyncFd::new(inner)?;
Ok(X11Producer(async_fd))
}
}
impl Inner {
fn decode(&mut self, xevent: xlib::XEvent) -> Option<(u32, Event)> {
log::info!("decoding {xevent:?}");
match xevent.get_type() {
t if t == KeyPress || t == KeyRelease => {
let key_event: xlib::XKeyEvent = unsafe { xevent.key };
let code = key_event.keycode;
let linux_code = code - 8;
let state = (xevent.get_type() == KeyPress) as u8;
return Some((
0,
Event::Keyboard(crate::event::KeyboardEvent::Key {
time: 0,
key: linux_code,
state,
}),
));
}
t if t == EnterNotify => {
let mut prev_win = 0;
unsafe {
XGetInputFocus(
self.display,
&mut self.window as *mut _,
&mut prev_win as *mut _,
);
XGrabKeyboard(
self.display,
XDefaultRootWindow(self.display),
true as i32,
GrabModeAsync,
GrabModeAsync,
CurrentTime,
);
XGrabPointer(
self.display,
self.window, /* window to grab */
true as i32, /* owner_events */
(PointerMotionMask | ButtonPressMask | ButtonReleaseMask) as u32, /* event mask */
GrabModeAsync, /* pointer_mode */
GrabModeAsync, /* keyboard_mode */
self.window, /* confine_to */
0, /* cursor */
CurrentTime,
);
};
Some((0, Event::Enter()))
}
t if t == MotionNotify => {
let pointer_event = unsafe { xevent.motion };
let (abs_x, abs_y) = (pointer_event.x, pointer_event.y);
let event = Event::Pointer(PointerEvent::Motion {
time: 0,
relative_x: abs_x as f64,
relative_y: abs_y as f64,
});
Some((0, event))
}
t if t == ButtonPress || t == ButtonRelease => {
let button_event = unsafe { xevent.button };
log::info!("{:?}", xevent);
Some((0, Event::Pointer(PointerEvent::Button {
time: 0,
button: button_event.button,
state: button_event.state,
})))
}
_ => None,
}
}
fn dispatch(&mut self) -> io::Result<bool> {
unsafe {
if XPending(self.display) > 0 {
let mut xevent: xlib::XEvent = std::mem::zeroed();
XNextEvent(self.display, &mut xevent as *mut _);
if let Some(event) = self.decode(xevent) {
self.pending_events.push_back(event);
}
Ok(true)
} else {
Ok(false)
}
}
}
}
impl Drop for Inner {
fn drop(&mut self) {
unsafe {
XCloseDisplay(self.display);
}
}
}
impl EventProducer for X11Producer {
fn notify(&mut self, _event: ClientEvent) -> io::Result<()> {
Ok(())
}
fn release(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Stream for X11Producer {
type Item = io::Result<(ClientHandle, Event)>;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<Option<Self::Item>> {
if let Some(event) = self.0.get_mut().pending_events.pop_front() {
return Poll::Ready(Some(Ok(event)));
}
loop {
let mut guard = ready!(self.0.poll_read_ready_mut(cx))?;
{
let inner = guard.get_inner_mut();
loop {
if match inner.dispatch() {
Ok(event) => event,
Err(e) => {
guard.clear_ready();
return Poll::Ready(Some(Err(e)));
}
} == false
{
break;
}
}
}
guard.clear_ready();
match guard.get_inner_mut().pending_events.pop_front() {
Some(event) => {
return Poll::Ready(Some(Ok(event)));
}
None => continue,
}
}
}
}

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

View File

@@ -46,20 +46,6 @@ impl Display for Position {
} }
} }
impl TryFrom<&str> for Position {
type Error = ();
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
"left" => Ok(Position::Left),
"right" => Ok(Position::Right),
"top" => Ok(Position::Top),
"bottom" => Ok(Position::Bottom),
_ => Err(()),
}
}
}
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub struct Client { pub struct Client {
/// hostname of this client /// hostname of this client
@@ -67,13 +53,13 @@ pub struct Client {
/// 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. /// unique handle to refer to the client.
/// This way any emulation / capture backend does not /// This way any event consumer / producer backend does not
/// need to know anything about a client other than its handle. /// need to know anything about a client other than its handle.
pub handle: ClientHandle, pub handle: ClientHandle,
/// all ip addresses associated with a particular client /// all socket addresses associated with a particular client
/// e.g. Laptops usually have at least an ethernet and a wifi port /// e.g. Laptops usually have at least an ethernet and a wifi port
/// which have different ip addresses /// which have different ip addresses
pub ips: HashSet<IpAddr>, pub addrs: HashSet<SocketAddr>,
/// 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
@@ -126,7 +112,6 @@ impl ClientManager {
ips: HashSet<IpAddr>, ips: HashSet<IpAddr>,
port: u16, port: u16,
pos: Position, pos: Position,
active: bool,
) -> ClientHandle { ) -> ClientHandle {
// get a new client_handle // get a new client_handle
let handle = self.free_id(); let handle = self.free_id();
@@ -134,12 +119,15 @@ impl ClientManager {
// store fix ip addresses // store fix ip addresses
let fix_ips = ips.iter().cloned().collect(); let fix_ips = ips.iter().cloned().collect();
// map ip addresses to socket addresses
let addrs = HashSet::from_iter(ips.into_iter().map(|ip| SocketAddr::new(ip, port)));
// store the client // store the client
let client = Client { let client = Client {
hostname, hostname,
fix_ips, fix_ips,
handle, handle,
ips, addrs,
port, port,
pos, pos,
}; };
@@ -147,7 +135,7 @@ impl ClientManager {
// client was never seen, nor pinged // client was never seen, nor pinged
let client_state = ClientState { let client_state = ClientState {
client, client,
active, active: false,
active_addr: None, active_addr: None,
alive: false, alive: false,
pressed_keys: HashSet::new(), pressed_keys: HashSet::new(),
@@ -170,7 +158,7 @@ impl ClientManager {
.iter() .iter()
.position(|c| { .position(|c| {
if let Some(c) = c { if let Some(c) = c {
c.active && c.client.ips.contains(&addr.ip()) c.active && c.client.addrs.contains(&addr)
} else { } else {
false false
} }

View File

@@ -8,8 +8,6 @@ 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};
pub const DEFAULT_PORT: u16 = 4242; pub const DEFAULT_PORT: u16 = 4242;
@@ -17,20 +15,17 @@ pub const DEFAULT_PORT: u16 = 4242;
pub struct ConfigToml { pub struct ConfigToml {
pub port: Option<u16>, pub port: Option<u16>,
pub frontend: Option<String>, pub frontend: Option<String>,
pub release_bind: Option<Vec<scancode::Linux>>, pub left: Option<Client>,
pub left: Option<TomlClient>, pub right: Option<Client>,
pub right: Option<TomlClient>, pub top: Option<Client>,
pub top: Option<TomlClient>, pub bottom: Option<Client>,
pub bottom: Option<TomlClient>,
} }
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct TomlClient { pub struct Client {
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>,
} }
impl ConfigToml { impl ConfigToml {
@@ -71,22 +66,10 @@ pub enum Frontend {
pub struct Config { pub struct Config {
pub frontend: Frontend, pub frontend: Frontend,
pub port: u16, pub port: u16,
pub clients: Vec<(TomlClient, Position)>, pub clients: Vec<(Client, Position)>,
pub daemon: bool, pub daemon: bool,
pub release_bind: Vec<scancode::Linux>,
} }
pub struct ConfigClient {
pub ips: HashSet<IpAddr>,
pub hostname: Option<String>,
pub port: u16,
pub pos: Position,
pub active: bool,
}
const DEFAULT_RELEASE_KEYS: [scancode::Linux; 4] =
[KeyLeftCtrl, KeyLeftShift, KeyLeftMeta, KeyLeftAlt];
impl Config { impl Config {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
let args = CliArgs::parse(); let args = CliArgs::parse();
@@ -110,7 +93,7 @@ impl Config {
let config_toml = match ConfigToml::new(config_path.as_str()) { let config_toml = match ConfigToml::new(config_path.as_str()) {
Err(e) => { Err(e) => {
log::warn!("{config_path}: {e}"); log::error!("{config_path}: {e}");
log::warn!("Continuing without config file ..."); log::warn!("Continuing without config file ...");
None None
} }
@@ -145,13 +128,7 @@ impl Config {
}, },
}; };
log::debug!("{config_toml:?}"); let mut clients: Vec<(Client, Position)> = vec![];
let release_bind = config_toml
.as_ref()
.and_then(|c| c.release_bind.clone())
.unwrap_or(Vec::from_iter(DEFAULT_RELEASE_KEYS.iter().cloned()));
let mut clients: Vec<(TomlClient, Position)> = vec![];
if let Some(config_toml) = config_toml { if let Some(config_toml) = config_toml {
if let Some(c) = config_toml.right { if let Some(c) = config_toml.right {
@@ -175,32 +152,21 @@ impl Config {
frontend, frontend,
clients, clients,
port, port,
release_bind,
}) })
} }
pub fn get_clients(&self) -> Vec<ConfigClient> { pub fn get_clients(&self) -> Vec<(HashSet<IpAddr>, Option<String>, u16, Position)> {
self.clients self.clients
.iter() .iter()
.map(|(c, pos)| { .map(|(c, p)| {
let port = c.port.unwrap_or(DEFAULT_PORT); let port = c.port.unwrap_or(DEFAULT_PORT);
let ips: HashSet<IpAddr> = if let Some(ips) = c.ips.as_ref() { let ips: HashSet<IpAddr> = if let Some(ips) = c.ips.as_ref() {
HashSet::from_iter(ips.iter().cloned()) HashSet::from_iter(ips.iter().cloned())
} else { } else {
HashSet::new() HashSet::new()
}; };
let hostname = match &c.hostname { let host_name = c.host_name.clone();
Some(h) => Some(h.clone()), (ips, host_name, port, *p)
None => c.host_name.clone(),
};
let active = c.activate_on_startup.unwrap_or(false);
ConfigClient {
ips,
hostname,
port,
pos: *pos,
active,
}
}) })
.collect() .collect()
} }

78
src/consumer.rs Normal file
View File

@@ -0,0 +1,78 @@
use async_trait::async_trait;
use std::future;
use crate::{
backend::consumer,
client::{ClientEvent, ClientHandle},
event::Event,
};
use anyhow::Result;
#[async_trait]
pub trait EventConsumer: 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 EventConsumer> {
#[cfg(windows)]
match consumer::windows::WindowsConsumer::new() {
Ok(c) => return Box::new(c),
Err(e) => log::warn!("windows event consumer unavailable: {e}"),
}
#[cfg(target_os = "macos")]
match consumer::macos::MacOSConsumer::new() {
Ok(c) => {
log::info!("using macos event consumer");
return Box::new(c);
}
Err(e) => log::error!("macos consumer not available: {e}"),
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
match consumer::wlroots::WlrootsConsumer::new() {
Ok(c) => {
log::info!("using wlroots event consumer");
return Box::new(c);
}
Err(e) => log::info!("wayland backend not available: {e}"),
}
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
match consumer::libei::LibeiConsumer::new().await {
Ok(c) => {
log::info!("using libei event consumer");
return Box::new(c);
}
Err(e) => log::info!("libei not available: {e}"),
}
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
match consumer::xdg_desktop_portal::DesktopPortalConsumer::new().await {
Ok(c) => {
log::info!("using xdg-remote-desktop-portal event consumer");
return Box::new(c);
}
Err(e) => log::info!("remote desktop portal not available: {e}"),
}
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
match consumer::x11::X11Consumer::new() {
Ok(c) => {
log::info!("using x11 event consumer");
return Box::new(c);
}
Err(e) => log::info!("x11 consumer not available: {e}"),
}
log::error!("falling back to dummy event consumer");
Box::new(consumer::dummy::DummyConsumer::new())
}

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

@@ -11,7 +11,7 @@ pub const BTN_MIDDLE: u32 = 0x112;
pub const BTN_BACK: u32 = 0x113; pub const BTN_BACK: u32 = 0x113;
pub const BTN_FORWARD: u32 = 0x114; pub const BTN_FORWARD: u32 = 0x114;
#[derive(Debug, PartialEq, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum PointerEvent { pub enum PointerEvent {
Motion { Motion {
time: u32, time: u32,
@@ -31,7 +31,7 @@ pub enum PointerEvent {
Frame {}, Frame {},
} }
#[derive(Debug, PartialEq, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum KeyboardEvent { pub enum KeyboardEvent {
Key { Key {
time: u32, time: u32,
@@ -46,7 +46,7 @@ pub enum KeyboardEvent {
}, },
} }
#[derive(PartialEq, Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum Event { pub enum Event {
/// pointer event (motion / button / axis) /// pointer event (motion / button / axis)
Pointer(PointerEvent), Pointer(PointerEvent),

View File

@@ -103,9 +103,8 @@ pub enum FrontendEvent {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FrontendNotify { pub enum FrontendNotify {
NotifyClientActivate(ClientHandle, bool), NotifyClientCreate(ClientHandle, Option<String>, u16, Position),
NotifyClientCreate(Client), NotifyClientUpdate(ClientHandle, Option<String>, u16, Position),
NotifyClientUpdate(Client),
NotifyClientDelete(ClientHandle), NotifyClientDelete(ClientHandle),
/// new port, reason of failure (if failed) /// new port, reason of failure (if failed)
NotifyPortChange(u16, Option<String>), NotifyPortChange(u16, Option<String>),
@@ -225,6 +224,7 @@ impl FrontendListener {
log::debug!("json: {json}, len: {}", payload.len()); log::debug!("json: {json}, len: {}", payload.len());
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() {
// write len + payload // write len + payload

View File

@@ -83,26 +83,17 @@ pub fn run() -> Result<()> {
Err(e) => break log::error!("{e}"), Err(e) => break log::error!("{e}"),
}; };
match notify { match notify {
FrontendNotify::NotifyClientActivate(handle, active) => { FrontendNotify::NotifyClientCreate(client, host, port, pos) => {
if active { log::info!(
log::info!("client {handle} activated"); "new client ({client}): {}:{port} - {pos}",
} else { host.as_deref().unwrap_or("")
log::info!("client {handle} deactivated"); );
}
} }
FrontendNotify::NotifyClientCreate(client) => { FrontendNotify::NotifyClientUpdate(client, host, port, pos) => {
let handle = client.handle; log::info!(
let port = client.port; "client ({client}) updated: {}:{port} - {pos}",
let pos = client.pos; host.as_deref().unwrap_or("")
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) => { FrontendNotify::NotifyClientDelete(client) => {
log::info!("client ({client}) deleted."); log::info!("client ({client}) deleted.");
@@ -118,7 +109,7 @@ pub fn run() -> Result<()> {
client.hostname.as_deref().unwrap_or(""), client.hostname.as_deref().unwrap_or(""),
if active { "yes" } else { "no" }, if active { "yes" } else { "no" },
client client
.ips .addrs
.into_iter() .into_iter()
.map(|a| a.to_string()) .map(|a| a.to_string())
.collect::<Vec<String>>() .collect::<Vec<String>>()

View File

@@ -8,12 +8,16 @@ use std::{
process, str, process, str,
}; };
use crate::frontend::gtk::window::Window; use crate::{config::DEFAULT_PORT, frontend::gtk::window::Window};
use adw::Application; use adw::Application;
use gtk::{ use gtk::{
gdk::Display, glib::clone, prelude::*, subclass::prelude::ObjectSubclassIsExt, CssProvider, gdk::Display,
IconTheme, gio::{SimpleAction, SimpleActionGroup},
glib::clone,
prelude::*,
subclass::prelude::ObjectSubclassIsExt,
CssProvider, IconTheme,
}; };
use gtk::{gio, glib, prelude::ApplicationExt}; use gtk::{gio, glib, prelude::ApplicationExt};
@@ -34,12 +38,7 @@ pub fn run() -> glib::ExitCode {
#[cfg(not(windows))] #[cfg(not(windows))]
let ret = gtk_main(); let ret = gtk_main();
if ret == glib::ExitCode::FAILURE { log::debug!("frontend exited");
log::error!("frontend exited with failure");
} else {
log::info!("frontend exited successfully");
}
ret ret
} }
@@ -47,7 +46,7 @@ fn gtk_main() -> glib::ExitCode {
gio::resources_register_include!("lan-mouse.gresource").expect("Failed to register resources."); gio::resources_register_include!("lan-mouse.gresource").expect("Failed to register resources.");
let app = Application::builder() let app = Application::builder()
.application_id("de.feschber.LanMouse") .application_id("de.feschber.lan-mouse")
.build(); .build();
app.connect_startup(|_| load_icons()); app.connect_startup(|_| load_icons());
@@ -69,8 +68,8 @@ fn load_css() {
} }
fn load_icons() { fn load_icons() {
let display = &Display::default().expect("Could not connect to a display."); let icon_theme =
let icon_theme = IconTheme::for_display(display); IconTheme::for_display(&Display::default().expect("Could not connect to a display."));
icon_theme.add_resource_path("/de/feschber/LanMouse/icons"); icon_theme.add_resource_path("/de/feschber/LanMouse/icons");
} }
@@ -129,19 +128,17 @@ fn build_ui(app: &Application) {
window.imp().stream.borrow_mut().replace(tx); window.imp().stream.borrow_mut().replace(tx);
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();
match notify { match notify {
FrontendNotify::NotifyClientActivate(handle, active) => { FrontendNotify::NotifyClientCreate(client, hostname, port, position) => {
window.activate_client(handle, active); window.new_client(client, hostname, port, position, false);
}
FrontendNotify::NotifyClientCreate(client) => {
window.new_client(client, false);
}, },
FrontendNotify::NotifyClientUpdate(client) => { FrontendNotify::NotifyClientUpdate(client, hostname, port, position) => {
window.update_client(client); log::info!("client updated: {client}, {}:{port}, {position}", hostname.unwrap_or("".to_string()));
} }
FrontendNotify::NotifyError(e) => { FrontendNotify::NotifyError(e) => {
window.show_toast(e.as_str()); // TODO
log::error!("{e}");
}, },
FrontendNotify::NotifyClientDelete(client) => { FrontendNotify::NotifyClientDelete(client) => {
window.delete_client(client); window.delete_client(client);
@@ -149,11 +146,19 @@ fn build_ui(app: &Application) {
FrontendNotify::Enumerate(clients) => { FrontendNotify::Enumerate(clients) => {
for (client, active) in clients { for (client, active) in clients {
if window.client_idx(client.handle).is_some() { if window.client_idx(client.handle).is_some() {
window.activate_client(client.handle, active); continue
window.update_client(client);
} else {
window.new_client(client, active);
} }
window.new_client(
client.handle,
client.hostname,
client.addrs
.iter()
.next()
.map(|s| s.port())
.unwrap_or(DEFAULT_PORT),
client.pos,
active,
);
} }
}, },
FrontendNotify::NotifyPortChange(port, msg) => { FrontendNotify::NotifyPortChange(port, msg) => {
@@ -167,5 +172,37 @@ fn build_ui(app: &Application) {
} }
})); }));
let action_request_client_update =
SimpleAction::new("request-client-update", Some(&u32::static_variant_type()));
// remove client
let action_client_delete =
SimpleAction::new("request-client-delete", Some(&u32::static_variant_type()));
// update client state
action_request_client_update.connect_activate(clone!(@weak window => move |_action, param| {
log::debug!("request-client-update");
let index = param.unwrap()
.get::<u32>()
.unwrap();
let Some(client) = window.clients().item(index) else {
return;
};
let client = client.downcast_ref::<ClientObject>().unwrap();
window.request_client_update(client);
}));
action_client_delete.connect_activate(clone!(@weak window => move |_action, param| {
log::debug!("delete-client");
let idx = param.unwrap()
.get::<u32>()
.unwrap();
window.request_client_delete(idx);
}));
let actions = SimpleActionGroup::new();
window.insert_action_group("win", Some(&actions));
actions.add_action(&action_request_client_update);
actions.add_action(&action_client_delete);
window.present(); window.present();
} }

View File

@@ -3,20 +3,26 @@ 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::ClientHandle;
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,
hostname: Option<String>,
port: u32,
position: String,
active: bool,
) -> Self {
Object::builder() Object::builder()
.property("handle", client.handle) .property("handle", handle)
.property("hostname", client.hostname) .property("hostname", hostname)
.property("port", client.port as u32) .property("port", port)
.property("position", client.pos.to_string())
.property("active", active) .property("active", active)
.property("position", position)
.build() .build()
} }

View File

@@ -28,12 +28,6 @@ impl ClientRow {
.sync_create() .sync_create()
.build(); .build();
let switch_position_binding = client_object
.bind_property("active", &self.imp().enable_switch.get(), "active")
.bidirectional()
.sync_create()
.build();
let hostname_binding = client_object let hostname_binding = client_object
.bind_property("hostname", &self.imp().hostname.get(), "text") .bind_property("hostname", &self.imp().hostname.get(), "text")
.transform_to(|_, v: Option<String>| { .transform_to(|_, v: Option<String>| {
@@ -110,7 +104,6 @@ impl ClientRow {
.build(); .build();
bindings.push(active_binding); bindings.push(active_binding);
bindings.push(switch_position_binding);
bindings.push(hostname_binding); bindings.push(hostname_binding);
bindings.push(title_binding); bindings.push(title_binding);
bindings.push(port_binding); bindings.push(port_binding);

View File

@@ -4,9 +4,7 @@ use adw::subclass::prelude::*;
use adw::{prelude::*, ActionRow, ComboRow}; use adw::{prelude::*, ActionRow, ComboRow};
use glib::{subclass::InitializingObject, Binding}; use glib::{subclass::InitializingObject, Binding};
use gtk::glib::clone; use gtk::glib::clone;
use gtk::glib::subclass::Signal;
use gtk::{glib, Button, CompositeTemplate, Switch}; use gtk::{glib, Button, CompositeTemplate, Switch};
use std::sync::OnceLock;
#[derive(CompositeTemplate, Default)] #[derive(CompositeTemplate, Default)]
#[template(resource = "/de/feschber/LanMouse/client_row.ui")] #[template(resource = "/de/feschber/LanMouse/client_row.ui")]
@@ -30,8 +28,6 @@ pub struct ClientRow {
impl ObjectSubclass for ClientRow { impl ObjectSubclass for ClientRow {
// `NAME` needs to match `class` attribute of template // `NAME` needs to match `class` attribute of template
const NAME: &'static str = "ClientRow"; const NAME: &'static str = "ClientRow";
const ABSTRACT: bool = false;
type Type = super::ClientRow; type Type = super::ClientRow;
type ParentType = adw::ExpanderRow; type ParentType = adw::ExpanderRow;
@@ -53,33 +49,28 @@ impl ObjectImpl for ClientRow {
row.handle_client_delete(button); row.handle_client_delete(button);
})); }));
} }
fn signals() -> &'static [glib::subclass::Signal] {
static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
SIGNALS.get_or_init(|| {
vec![
Signal::builder("request-update")
.param_types([bool::static_type()])
.build(),
Signal::builder("request-delete").build(),
]
})
}
} }
#[gtk::template_callbacks] #[gtk::template_callbacks]
impl ClientRow { impl ClientRow {
#[template_callback] #[template_callback]
fn handle_client_set_state(&self, state: bool, _switch: &Switch) -> bool { fn handle_client_set_state(&self, state: bool, switch: &Switch) -> bool {
log::debug!("state change -> requesting update"); let idx = self.obj().index() as u32;
self.obj().emit_by_name::<()>("request-update", &[&state]); switch
.activate_action("win.request-client-update", Some(&idx.to_variant()))
.unwrap();
switch.set_state(state);
true // dont run default handler true // dont run default handler
} }
#[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");
self.obj().emit_by_name::<()>("request-delete", &[]); let idx = self.obj().index() as u32;
button
.activate_action("win.request-client-delete", Some(&idx.to_variant()))
.unwrap();
} }
} }

View File

@@ -5,14 +5,10 @@ use std::io::Write;
use adw::prelude::*; use adw::prelude::*;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use glib::{clone, Object}; use glib::{clone, Object};
use gtk::{ use gtk::{gio, glib, NoSelection};
gio,
glib::{self, closure_local},
NoSelection,
};
use crate::{ use crate::{
client::{Client, ClientHandle, Position}, client::{ClientHandle, Position},
config::DEFAULT_PORT, config::DEFAULT_PORT,
frontend::{gtk::client_object::ClientObject, FrontendEvent}, frontend::{gtk::client_object::ClientObject, FrontendEvent},
}; };
@@ -49,18 +45,6 @@ impl Window {
clone!(@weak self as window => @default-panic, move |obj| { clone!(@weak self as window => @default-panic, move |obj| {
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| {
let index = row.index() as u32;
let Some(client) = window.clients().item(index) else {
return;
};
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| {
let index = row.index() as u32;
window.request_client_delete(index);
}));
row.upcast() row.upcast()
}) })
); );
@@ -78,7 +62,7 @@ impl Window {
} }
fn setup_icon(&self) { fn setup_icon(&self) {
self.set_icon_name(Some("de.feschber.LanMouse")); self.set_icon_name(Some("mouse-icon"));
} }
fn create_client_row(&self, client_object: &ClientObject) -> ClientRow { fn create_client_row(&self, client_object: &ClientObject) -> ClientRow {
@@ -87,8 +71,15 @@ impl Window {
row row
} }
pub fn new_client(&self, client: Client, active: bool) { pub fn new_client(
let client = ClientObject::new(client, active); &self,
handle: ClientHandle,
hostname: Option<String>,
port: u16,
position: Position,
active: bool,
) {
let client = ClientObject::new(handle, hostname, port as u32, position.to_string(), active);
self.clients().append(&client); self.clients().append(&client);
self.set_placeholder_visible(false); self.set_placeholder_visible(false);
} }
@@ -115,42 +106,6 @@ impl Window {
} }
} }
pub fn update_client(&self, client: Client) {
let Some(idx) = self.client_idx(client.handle) else {
log::warn!("could not find client with handle {}", client.handle);
return;
};
let client_object = self.clients().item(idx as u32).unwrap();
let client_object: &ClientObject = client_object.downcast_ref().unwrap();
let data = client_object.get_data();
/* only change if it actually has changed, otherwise
* the update signal is triggered */
if data.hostname != client.hostname {
client_object.set_hostname(client.hostname.unwrap_or("".into()));
}
if data.port != client.port as u32 {
client_object.set_port(client.port as u32);
}
if data.position != client.pos.to_string() {
client_object.set_position(client.pos.to_string());
}
}
pub fn activate_client(&self, handle: ClientHandle, active: bool) {
let Some(idx) = self.client_idx(handle) else {
log::warn!("could not find client with handle {handle}");
return;
};
let client_object = self.clients().item(idx as u32).unwrap();
let client_object: &ClientObject = client_object.downcast_ref().unwrap();
let data = client_object.get_data();
if data.active != active {
client_object.set_active(active);
log::debug!("set active to {active}");
}
}
pub fn request_client_create(&self) { pub fn request_client_create(&self) {
let event = FrontendEvent::AddClient(None, DEFAULT_PORT, Position::default()); let event = FrontendEvent::AddClient(None, DEFAULT_PORT, Position::default());
self.imp().set_port(DEFAULT_PORT); self.imp().set_port(DEFAULT_PORT);
@@ -166,10 +121,13 @@ impl Window {
} }
} }
pub fn request_client_update(&self, client: &ClientObject, active: bool) { pub fn request_client_update(&self, client: &ClientObject) {
let data = client.get_data(); let data = client.get_data();
let position = match Position::try_from(data.position.as_str()) { let position = match data.position.as_str() {
Ok(pos) => pos, "left" => Position::Left,
"right" => Position::Right,
"top" => Position::Top,
"bottom" => Position::Bottom,
_ => { _ => {
log::error!("invalid position: {}", data.position); log::error!("invalid position: {}", data.position);
return; return;
@@ -177,13 +135,10 @@ impl Window {
}; };
let hostname = data.hostname; let hostname = data.hostname;
let port = data.port as u16; let port = data.port as u16;
let event = FrontendEvent::UpdateClient(client.handle(), hostname, port, position); let event = FrontendEvent::UpdateClient(client.handle(), hostname, port, position);
log::debug!("requesting update: {event:?}");
self.request(event); self.request(event);
let event = FrontendEvent::ActivateClient(client.handle(), active); let event = FrontendEvent::ActivateClient(client.handle(), !client.active());
log::debug!("requesting activate: {event:?}");
self.request(event); self.request(event);
} }

View File

@@ -6,7 +6,10 @@ use std::net::TcpStream;
use std::os::unix::net::UnixStream; use std::os::unix::net::UnixStream;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use adw::{prelude::*, ActionRow, ToastOverlay}; use adw::{
prelude::{EditableExt, WidgetExt},
ActionRow, ToastOverlay,
};
use glib::subclass::InitializingObject; use glib::subclass::InitializingObject;
use gtk::{gio, glib, Button, CompositeTemplate, Entry, ListBox}; use gtk::{gio, glib, Button, CompositeTemplate, Entry, ListBox};
@@ -39,8 +42,6 @@ pub struct Window {
impl ObjectSubclass for Window { impl ObjectSubclass for Window {
// `NAME` needs to match `class` attribute of template // `NAME` needs to match `class` attribute of template
const NAME: &'static str = "LanMouseWindow"; const NAME: &'static str = "LanMouseWindow";
const ABSTRACT: bool = false;
type Type = super::Window; type Type = super::Window;
type ParentType = adw::ApplicationWindow; type ParentType = adw::ApplicationWindow;

View File

@@ -4,8 +4,9 @@ pub mod dns;
pub mod event; pub mod event;
pub mod server; pub mod server;
pub mod capture; pub mod consumer;
pub mod emulate; pub mod producer;
pub mod backend;
pub mod frontend; pub mod frontend;
pub mod scancode; pub mod scancode;

View File

@@ -29,7 +29,6 @@ pub fn run() -> Result<()> {
// parse config file + cli args // parse config file + cli args
let config = Config::new()?; let config = Config::new()?;
log::debug!("{config:?}"); log::debug!("{config:?}");
log::info!("release bind: {:?}", config.release_bind);
if config.daemon { if config.daemon {
// if daemon is specified we run the service // if daemon is specified we run the service
@@ -39,6 +38,7 @@ pub fn run() -> Result<()> {
// run a frontend // run a frontend
let mut service = start_service()?; let mut service = start_service()?;
frontend::run_frontend(&config)?; frontend::run_frontend(&config)?;
log::info!("terminating service");
#[cfg(unix)] #[cfg(unix)]
{ {
// on unix we give the service a chance to terminate gracefully // on unix we give the service a chance to terminate gracefully

61
src/producer.rs Normal file
View File

@@ -0,0 +1,61 @@
use std::io;
use futures_core::Stream;
use crate::backend::producer;
use crate::{
client::{ClientEvent, ClientHandle},
event::Event,
};
pub async fn create() -> Box<dyn EventProducer> {
#[cfg(target_os = "macos")]
match producer::macos::MacOSProducer::new() {
Ok(p) => return Box::new(p),
Err(e) => log::info!("macos event producer not available: {e}"),
}
#[cfg(windows)]
match producer::windows::WindowsProducer::new() {
Ok(p) => return Box::new(p),
Err(e) => log::info!("windows event producer not available: {e}"),
}
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
match producer::libei::LibeiProducer::new() {
Ok(p) => {
log::info!("using libei event producer");
return Box::new(p);
}
Err(e) => log::info!("libei event producer not available: {e}"),
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
match producer::wayland::WaylandEventProducer::new() {
Ok(p) => {
log::info!("using layer-shell event producer");
return Box::new(p);
}
Err(e) => log::info!("layer_shell event producer not available: {e}"),
}
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
match producer::x11::X11Producer::new() {
Ok(p) => {
log::info!("using x11 event producer");
return Box::new(p);
}
Err(e) => log::info!("x11 event producer not available: {e}"),
}
log::error!("falling back to dummy event producer");
Box::new(producer::dummy::DummyProducer::new())
}
pub trait EventProducer: Stream<Item = io::Result<(ClientHandle, Event)>> + Unpin {
/// notify event producer of configuration changes
fn notify(&mut self, event: ClientEvent) -> io::Result<()>;
/// release mouse
fn release(&mut self) -> io::Result<()>;
}

View File

@@ -1,5 +1,3 @@
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
*/ */
@@ -167,7 +165,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(Debug, Clone, Copy)]
#[allow(dead_code)] #[allow(dead_code)]
pub enum Linux { pub enum Linux {
KeyReserved = 0, KeyReserved = 0,
@@ -212,7 +210,7 @@ pub enum Linux {
KeySemicolon = 39, KeySemicolon = 39,
KeyApostrophe = 40, KeyApostrophe = 40,
KeyGrave = 41, KeyGrave = 41,
KeyLeftShift = 42, KeyLeftshift = 42,
KeyBackslash = 43, KeyBackslash = 43,
KeyZ = 44, KeyZ = 44,
KeyX = 45, KeyX = 45,
@@ -226,7 +224,7 @@ pub enum Linux {
KeySlash = 53, KeySlash = 53,
KeyRightShift = 54, KeyRightShift = 54,
KeyKpAsterisk = 55, KeyKpAsterisk = 55,
KeyLeftAlt = 56, KeyLeftalt = 56,
KeySpace = 57, KeySpace = 57,
KeyCapsLock = 58, KeyCapsLock = 58,
KeyF1 = 59, KeyF1 = 59,
@@ -296,7 +294,7 @@ pub enum Linux {
// KEY_HANGUEL = KeyHangeul, // KEY_HANGUEL = KeyHangeul,
KeyHanja = 123, KeyHanja = 123,
KeyYen = 124, KeyYen = 124,
KeyLeftMeta = 125, KeyLeftmeta = 125,
KeyRightmeta = 126, KeyRightmeta = 126,
KeyCompose = 127, KeyCompose = 127,
KeyStop = 128, /* AC Stop */ KeyStop = 128, /* AC Stop */
@@ -487,7 +485,7 @@ impl TryFrom<Linux> for Windows {
Linux::KeySemicolon => Ok(Self::KeySemiColon), Linux::KeySemicolon => Ok(Self::KeySemiColon),
Linux::KeyApostrophe => Ok(Self::KeyApostrophe), Linux::KeyApostrophe => Ok(Self::KeyApostrophe),
Linux::KeyGrave => Ok(Self::KeyGrave), Linux::KeyGrave => Ok(Self::KeyGrave),
Linux::KeyLeftShift => Ok(Self::KeyLeftShift), Linux::KeyLeftshift => Ok(Self::KeyLeftShift),
Linux::KeyBackslash => Ok(Self::KeyBackslash), Linux::KeyBackslash => Ok(Self::KeyBackslash),
Linux::KeyZ => Ok(Self::KeyZ), Linux::KeyZ => Ok(Self::KeyZ),
Linux::KeyX => Ok(Self::KeyX), Linux::KeyX => Ok(Self::KeyX),
@@ -501,7 +499,7 @@ impl TryFrom<Linux> for Windows {
Linux::KeySlash => Ok(Self::KeySlash), Linux::KeySlash => Ok(Self::KeySlash),
Linux::KeyRightShift => Ok(Self::KeyRightShift), Linux::KeyRightShift => Ok(Self::KeyRightShift),
Linux::KeyKpAsterisk => Ok(Self::KeypadStar), Linux::KeyKpAsterisk => Ok(Self::KeypadStar),
Linux::KeyLeftAlt => Ok(Self::KeyLeftAlt), Linux::KeyLeftalt => Ok(Self::KeyLeftAlt),
Linux::KeySpace => Ok(Self::KeySpace), Linux::KeySpace => Ok(Self::KeySpace),
Linux::KeyCapsLock => Ok(Self::KeyCapsLock), Linux::KeyCapsLock => Ok(Self::KeyCapsLock),
Linux::KeyF1 => Ok(Self::KeyF1), Linux::KeyF1 => Ok(Self::KeyF1),
@@ -569,7 +567,7 @@ impl TryFrom<Linux> for Windows {
Linux::KeyHangeul => Ok(Self::KeyInternational1), // TODO unsure Linux::KeyHangeul => Ok(Self::KeyInternational1), // TODO unsure
Linux::KeyHanja => Ok(Self::KeyInternational2), // TODO unsure Linux::KeyHanja => Ok(Self::KeyInternational2), // TODO unsure
Linux::KeyYen => Ok(Self::KeyInternational3), // TODO unsure Linux::KeyYen => Ok(Self::KeyInternational3), // TODO unsure
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),
Linux::KeyStop => Ok(Self::ACStop), Linux::KeyStop => Ok(Self::ACStop),

File diff suppressed because it is too large Load Diff

View File

@@ -1,152 +0,0 @@
use anyhow::{anyhow, Result};
use futures::StreamExt;
use std::{collections::HashSet, net::SocketAddr};
use tokio::{sync::mpsc::Sender, task::JoinHandle};
use crate::{
capture::InputCapture,
client::{ClientEvent, ClientHandle},
event::{Event, KeyboardEvent},
scancode,
server::State,
};
use super::Server;
#[derive(Clone, Copy, Debug)]
pub enum CaptureEvent {
/// capture must release the mouse
Release,
/// capture is notified of a change in client states
ClientEvent(ClientEvent),
/// termination signal
Terminate,
}
pub fn new(
mut capture: Box<dyn InputCapture>,
server: Server,
sender_tx: Sender<(Event, SocketAddr)>,
timer_tx: Sender<()>,
release_bind: Vec<scancode::Linux>,
) -> (JoinHandle<Result<()>>, Sender<CaptureEvent>) {
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
let task = tokio::task::spawn_local(async move {
let mut pressed_keys = HashSet::new();
loop {
tokio::select! {
event = capture.next() => {
match event {
Some(Ok(event)) => handle_capture_event(&server, &mut capture, &sender_tx, &timer_tx, event, &mut pressed_keys, &release_bind).await?,
Some(Err(e)) => return Err(anyhow!("input capture: {e:?}")),
None => return Err(anyhow!("input capture terminated")),
}
}
e = rx.recv() => {
log::debug!("input capture notify rx: {e:?}");
match e {
Some(e) => match e {
CaptureEvent::Release => {
capture.release()?;
server.state.replace(State::Receiving);
}
CaptureEvent::ClientEvent(e) => capture.notify(e)?,
CaptureEvent::Terminate => break,
},
None => break,
}
}
}
}
anyhow::Ok(())
});
(task, tx)
}
fn update_pressed_keys(pressed_keys: &mut HashSet<scancode::Linux>, key: u32, state: u8) {
if let Ok(scancode) = scancode::Linux::try_from(key) {
log::debug!("key: {key}, state: {state}, scancode: {scancode:?}");
match state {
1 => pressed_keys.insert(scancode),
_ => pressed_keys.remove(&scancode),
};
}
}
async fn handle_capture_event(
server: &Server,
capture: &mut Box<dyn InputCapture>,
sender_tx: &Sender<(Event, SocketAddr)>,
timer_tx: &Sender<()>,
event: (ClientHandle, Event),
pressed_keys: &mut HashSet<scancode::Linux>,
release_bind: &[scancode::Linux],
) -> Result<()> {
let (c, mut e) = event;
log::trace!("({c}) {e:?}");
if let Event::Keyboard(KeyboardEvent::Key { key, state, .. }) = e {
update_pressed_keys(pressed_keys, key, state);
log::debug!("{pressed_keys:?}");
if release_bind.iter().all(|k| pressed_keys.contains(k)) {
pressed_keys.clear();
log::info!("releasing pointer");
capture.release()?;
server.state.replace(State::Receiving);
log::trace!("STATE ===> Receiving");
// send an event to release all the modifiers
e = Event::Disconnect();
}
}
let (addr, enter, start_timer) = {
let mut enter = false;
let mut start_timer = false;
// get client state for handle
let mut client_manager = server.client_manager.borrow_mut();
let client_state = match client_manager.get_mut(c) {
Some(state) => state,
None => {
// should not happen
log::warn!("unknown client!");
capture.release()?;
server.state.replace(State::Receiving);
log::trace!("STATE ===> Receiving");
return Ok(());
}
};
// if we just entered the client we want to send additional enter events until
// we get a leave event
if let Event::Enter() = e {
server.state.replace(State::AwaitingLeave);
server
.active_client
.replace(Some(client_state.client.handle));
log::trace!("Active client => {}", client_state.client.handle);
start_timer = true;
log::trace!("STATE ===> AwaitingLeave");
enter = true;
} else {
// ignore any potential events in receiving mode
if server.state.get() == State::Receiving && e != Event::Disconnect() {
return Ok(());
}
}
(client_state.active_addr, enter, start_timer)
};
if start_timer {
let _ = timer_tx.try_send(());
}
if let Some(addr) = addr {
if enter {
let _ = sender_tx.send((Event::Enter(), addr)).await;
}
let _ = sender_tx.send((e, addr)).await;
}
Ok(())
}

View File

@@ -1,240 +0,0 @@
use anyhow::{anyhow, Result};
use std::net::SocketAddr;
use tokio::{
sync::mpsc::{Receiver, Sender},
task::JoinHandle,
};
use crate::{
client::{ClientEvent, ClientHandle},
emulate::InputEmulation,
event::{Event, KeyboardEvent},
scancode,
server::State,
};
use super::{CaptureEvent, Server};
#[derive(Clone, Debug)]
pub enum EmulationEvent {
/// input emulation is notified of a change in client states
ClientEvent(ClientEvent),
/// input emulation must release keys for client
ReleaseKeys(ClientHandle),
/// termination signal
Terminate,
}
pub fn new(
mut emulate: Box<dyn InputEmulation>,
server: Server,
mut udp_rx: Receiver<Result<(Event, SocketAddr)>>,
sender_tx: Sender<(Event, SocketAddr)>,
capture_tx: Sender<CaptureEvent>,
timer_tx: Sender<()>,
) -> (JoinHandle<Result<()>>, Sender<EmulationEvent>) {
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
let emulate_task = tokio::task::spawn_local(async move {
let mut last_ignored = None;
loop {
tokio::select! {
udp_event = udp_rx.recv() => {
let udp_event = udp_event.ok_or(anyhow!("receiver closed"))??;
handle_udp_rx(&server, &capture_tx, &mut emulate, &sender_tx, &mut last_ignored, udp_event, &timer_tx).await;
}
emulate_event = rx.recv() => {
match emulate_event {
Some(e) => match e {
EmulationEvent::ClientEvent(e) => emulate.notify(e).await,
EmulationEvent::ReleaseKeys(c) => release_keys(&server, &mut emulate, c).await,
EmulationEvent::Terminate => break,
},
None => break,
}
}
res = emulate.dispatch() => {
res?;
}
}
}
// release potentially still pressed keys
let clients = server
.client_manager
.borrow()
.get_client_states()
.map(|s| s.client.handle)
.collect::<Vec<_>>();
for client in clients {
release_keys(&server, &mut emulate, client).await;
}
// destroy emulator
emulate.destroy().await;
anyhow::Ok(())
});
(emulate_task, tx)
}
async fn handle_udp_rx(
server: &Server,
capture_tx: &Sender<CaptureEvent>,
emulate: &mut Box<dyn InputEmulation>,
sender_tx: &Sender<(Event, SocketAddr)>,
last_ignored: &mut Option<SocketAddr>,
event: (Event, SocketAddr),
timer_tx: &Sender<()>,
) {
let (event, addr) = event;
// get handle for addr
let handle = match server.client_manager.borrow().get_client(addr) {
Some(a) => a,
None => {
if last_ignored.is_none() || last_ignored.is_some() && last_ignored.unwrap() != addr {
log::warn!("ignoring events from client {addr}");
last_ignored.replace(addr);
}
return;
}
};
// next event can be logged as ignored again
last_ignored.take();
log::trace!("{:20} <-<-<-<------ {addr} ({handle})", event.to_string());
{
let mut client_manager = server.client_manager.borrow_mut();
let client_state = match client_manager.get_mut(handle) {
Some(s) => s,
None => {
log::error!("unknown handle");
return;
}
};
// reset ttl for client and
client_state.alive = true;
// set addr as new default for this client
client_state.active_addr = Some(addr);
}
match (event, addr) {
(Event::Pong(), _) => { /* ignore pong events */ }
(Event::Ping(), addr) => {
let _ = sender_tx.send((Event::Pong(), addr)).await;
}
(Event::Disconnect(), _) => {
release_keys(server, emulate, handle).await;
}
(event, addr) => {
// tell clients that we are ready to receive events
if let Event::Enter() = event {
let _ = sender_tx.send((Event::Leave(), addr)).await;
}
match server.state.get() {
State::Sending => {
if let Event::Leave() = event {
// ignore additional leave events that may
// have been sent for redundancy
} else {
// upon receiving any event, we go back to receiving mode
server.state.replace(State::Receiving);
let _ = capture_tx.send(CaptureEvent::Release).await;
log::trace!("STATE ===> Receiving");
}
}
State::Receiving => {
let mut ignore_event = false;
if let Event::Keyboard(KeyboardEvent::Key {
time: _,
key,
state,
}) = event
{
let mut client_manager = server.client_manager.borrow_mut();
let client_state =
if let Some(client_state) = client_manager.get_mut(handle) {
client_state
} else {
log::error!("unknown handle");
return;
};
if state == 0 {
// ignore release event if key not pressed
ignore_event = !client_state.pressed_keys.remove(&key);
} else {
// ignore press event if key not released
ignore_event = !client_state.pressed_keys.insert(key);
let _ = timer_tx.try_send(());
}
}
// ignore double press / release events to
// workaround buggy rdp backend.
if !ignore_event {
// consume event
emulate.consume(event, handle).await;
log::trace!("{event:?} => emulate");
}
}
State::AwaitingLeave => {
// we just entered the deadzone of a client, so
// we need to ignore events that may still
// be on the way until a leave event occurs
// telling us the client registered the enter
if let Event::Leave() = event {
server.state.replace(State::Sending);
log::trace!("STATE ===> Sending");
}
// entering a client that is waiting for a leave
// event should still be possible
if let Event::Enter() = event {
server.state.replace(State::Receiving);
let _ = capture_tx.send(CaptureEvent::Release).await;
log::trace!("STATE ===> Receiving");
}
}
}
}
}
}
async fn release_keys(
server: &Server,
emulate: &mut Box<dyn InputEmulation>,
client: ClientHandle,
) {
let keys = server
.client_manager
.borrow_mut()
.get_mut(client)
.iter_mut()
.flat_map(|s| s.pressed_keys.drain())
.collect::<Vec<_>>();
for key in keys {
let event = Event::Keyboard(KeyboardEvent::Key {
time: 0,
key,
state: 0,
});
emulate.consume(event, client).await;
if let Ok(key) = scancode::Linux::try_from(key) {
log::warn!("releasing stuck key: {key:?}");
}
}
let modifiers_event = KeyboardEvent::Modifiers {
mods_depressed: 0,
mods_latched: 0,
mods_locked: 0,
group: 0,
};
emulate
.consume(Event::Keyboard(modifiers_event), client)
.await;
}

View File

@@ -1,342 +0,0 @@
use std::{
collections::HashSet,
io::ErrorKind,
net::{IpAddr, SocketAddr},
};
#[cfg(unix)]
use tokio::net::UnixStream;
#[cfg(windows)]
use tokio::net::TcpStream;
use anyhow::{anyhow, Result};
use tokio::{
io::ReadHalf,
sync::mpsc::{Receiver, Sender},
task::JoinHandle,
};
use crate::{
client::{ClientEvent, ClientHandle, Position},
frontend::{self, FrontendEvent, FrontendListener, FrontendNotify},
};
use super::{
capture_task::CaptureEvent, emulation_task::EmulationEvent, resolver_task::DnsRequest, Server,
};
pub(crate) fn new(
mut frontend: FrontendListener,
mut notify_rx: Receiver<FrontendNotify>,
server: Server,
capture_notify: Sender<CaptureEvent>,
emulate_notify: Sender<EmulationEvent>,
resolve_ch: Sender<DnsRequest>,
port_tx: Sender<u16>,
) -> (JoinHandle<Result<()>>, Sender<FrontendEvent>) {
let (event_tx, mut event_rx) = tokio::sync::mpsc::channel(32);
let event_tx_clone = event_tx.clone();
let frontend_task = tokio::task::spawn_local(async move {
loop {
tokio::select! {
stream = frontend.accept() => {
let stream = match stream {
Ok(s) => s,
Err(e) => {
log::warn!("error accepting frontend connection: {e}");
continue;
}
};
handle_frontend_stream(&event_tx_clone, stream).await;
}
event = event_rx.recv() => {
let frontend_event = event.ok_or(anyhow!("frontend channel closed"))?;
if handle_frontend_event(&server, &capture_notify, &emulate_notify, &resolve_ch, &mut frontend, &port_tx, frontend_event).await {
break;
}
}
notify = notify_rx.recv() => {
let notify = notify.ok_or(anyhow!("frontend notify closed"))?;
let _ = frontend.notify_all(notify).await;
}
}
}
anyhow::Ok(())
});
(frontend_task, event_tx)
}
async fn handle_frontend_stream(
frontend_tx: &Sender<FrontendEvent>,
#[cfg(unix)] mut stream: ReadHalf<UnixStream>,
#[cfg(windows)] mut stream: ReadHalf<TcpStream>,
) {
use std::io;
let tx = frontend_tx.clone();
tokio::task::spawn_local(async move {
let _ = tx.send(FrontendEvent::Enumerate()).await;
loop {
let event = frontend::read_event(&mut stream).await;
match event {
Ok(event) => {
let _ = tx.send(event).await;
}
Err(e) => {
if let Some(e) = e.downcast_ref::<io::Error>() {
if e.kind() == ErrorKind::UnexpectedEof {
return;
}
}
log::error!("error reading frontend event: {e}");
return;
}
}
}
});
}
async fn handle_frontend_event(
server: &Server,
capture_tx: &Sender<CaptureEvent>,
emulate_tx: &Sender<EmulationEvent>,
resolve_tx: &Sender<DnsRequest>,
frontend: &mut FrontendListener,
port_tx: &Sender<u16>,
event: FrontendEvent,
) -> bool {
log::debug!("frontend: {event:?}");
let response = match event {
FrontendEvent::AddClient(hostname, port, pos) => {
let handle = add_client(server, resolve_tx, hostname, HashSet::new(), port, pos).await;
let client = server
.client_manager
.borrow()
.get(handle)
.unwrap()
.client
.clone();
Some(FrontendNotify::NotifyClientCreate(client))
}
FrontendEvent::ActivateClient(handle, active) => {
activate_client(server, capture_tx, emulate_tx, handle, active).await;
Some(FrontendNotify::NotifyClientActivate(handle, active))
}
FrontendEvent::ChangePort(port) => {
let _ = port_tx.send(port).await;
None
}
FrontendEvent::DelClient(handle) => {
remove_client(server, capture_tx, emulate_tx, frontend, handle).await;
Some(FrontendNotify::NotifyClientDelete(handle))
}
FrontendEvent::Enumerate() => {
let clients = server
.client_manager
.borrow()
.get_client_states()
.map(|s| (s.client.clone(), s.active))
.collect();
Some(FrontendNotify::Enumerate(clients))
}
FrontendEvent::Shutdown() => {
log::info!("terminating gracefully...");
return true;
}
FrontendEvent::UpdateClient(handle, hostname, port, pos) => {
update_client(
server,
capture_tx,
emulate_tx,
resolve_tx,
(handle, hostname, port, pos),
)
.await;
let client = server
.client_manager
.borrow()
.get(handle)
.unwrap()
.client
.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
}
pub async fn add_client(
server: &Server,
resolver_tx: &Sender<DnsRequest>,
hostname: Option<String>,
addr: HashSet<IpAddr>,
port: u16,
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 {
let _ = resolver_tx.send(DnsRequest { hostname, handle }).await;
}
handle
}
pub async fn activate_client(
server: &Server,
capture_notify_tx: &Sender<CaptureEvent>,
emulate_notify_tx: &Sender<EmulationEvent>,
client: ClientHandle,
active: bool,
) {
let (client, pos) = match server.client_manager.borrow_mut().get_mut(client) {
Some(state) => {
state.active = active;
(state.client.handle, state.client.pos)
}
None => return,
};
if active {
let _ = capture_notify_tx
.send(CaptureEvent::ClientEvent(ClientEvent::Create(client, pos)))
.await;
let _ = emulate_notify_tx
.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;
}
}
pub async fn remove_client(
server: &Server,
capture_notify_tx: &Sender<CaptureEvent>,
emulate_notify_tx: &Sender<EmulationEvent>,
frontend: &mut FrontendListener,
client: ClientHandle,
) -> Option<ClientHandle> {
let Some((client, active)) = server
.client_manager
.borrow_mut()
.remove_client(client)
.map(|s| (s.client.handle, s.active))
else {
return None;
};
if active {
let _ = capture_notify_tx
.send(CaptureEvent::ClientEvent(ClientEvent::Destroy(client)))
.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(
server: &Server,
capture_notify_tx: &Sender<CaptureEvent>,
emulate_notify_tx: &Sender<EmulationEvent>,
resolve_tx: &Sender<DnsRequest>,
client_update: (ClientHandle, Option<String>, u16, Position),
) {
let (handle, hostname, port, pos) = client_update;
let mut changed = false;
let (hostname, handle, active) = {
// retrieve state
let mut client_manager = server.client_manager.borrow_mut();
let Some(state) = client_manager.get_mut(handle) else {
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
if state.client.hostname != hostname {
state.client.ips = HashSet::new();
state.active_addr = None;
state.client.hostname = hostname;
changed = true;
}
log::debug!("client updated: {:?}", state);
(
state.client.hostname.clone(),
state.client.handle,
state.active,
)
};
// resolve dns if something changed
if changed {
// resolve dns
if let Some(hostname) = hostname {
let _ = resolve_tx.send(DnsRequest { hostname, handle }).await;
}
}
// update state in event input emulator & input capture
if changed && active {
// update state
let _ = capture_notify_tx
.send(CaptureEvent::ClientEvent(ClientEvent::Destroy(handle)))
.await;
let _ = emulate_notify_tx
.send(EmulationEvent::ClientEvent(ClientEvent::Destroy(handle)))
.await;
let _ = capture_notify_tx
.send(CaptureEvent::ClientEvent(ClientEvent::Create(handle, pos)))
.await;
let _ = emulate_notify_tx
.send(EmulationEvent::ClientEvent(ClientEvent::Create(
handle, pos,
)))
.await;
}
}

View File

@@ -1,89 +0,0 @@
use std::net::SocketAddr;
use anyhow::Result;
use tokio::{
net::UdpSocket,
sync::mpsc::{Receiver, Sender},
task::JoinHandle,
};
use crate::{event::Event, frontend::FrontendNotify};
use super::Server;
pub async fn new(
server: Server,
frontend_notify_tx: Sender<FrontendNotify>,
) -> Result<(
JoinHandle<()>,
Sender<(Event, SocketAddr)>,
Receiver<Result<(Event, SocketAddr)>>,
Sender<u16>,
)> {
// bind the udp socket
let listen_addr = SocketAddr::new("0.0.0.0".parse().unwrap(), server.port.get());
let mut socket = UdpSocket::bind(listen_addr).await?;
let (receiver_tx, receiver_rx) = tokio::sync::mpsc::channel(32);
let (sender_tx, mut sender_rx) = tokio::sync::mpsc::channel(32);
let (port_tx, mut port_rx) = tokio::sync::mpsc::channel(32);
let udp_task = tokio::task::spawn_local(async move {
loop {
tokio::select! {
event = receive_event(&socket) => {
let _ = receiver_tx.send(event).await;
}
event = sender_rx.recv() => {
let Some((event, addr)) = event else {
break;
};
if let Err(e) = send_event(&socket, event, addr) {
log::warn!("udp send failed: {e}");
};
}
port = port_rx.recv() => {
let Some(port) = port else {
break;
};
if socket.local_addr().unwrap().port() == port {
continue;
}
let listen_addr = SocketAddr::new("0.0.0.0".parse().unwrap(), port);
match UdpSocket::bind(listen_addr).await {
Ok(new_socket) => {
socket = new_socket;
server.port.replace(port);
let _ = frontend_notify_tx.send(FrontendNotify::NotifyPortChange(port, None)).await;
}
Err(e) => {
log::warn!("could not change port: {e}");
let port = socket.local_addr().unwrap().port();
let _ = frontend_notify_tx.send(FrontendNotify::NotifyPortChange(
port,
Some(format!("could not change port: {e}")),
)).await;
}
}
}
}
}
});
Ok((udp_task, sender_tx, receiver_rx, port_tx))
}
async fn receive_event(socket: &UdpSocket) -> Result<(Event, SocketAddr)> {
let mut buf = vec![0u8; 22];
let (_amt, src) = socket.recv_from(&mut buf).await?;
Ok((Event::try_from(buf)?, src))
}
fn send_event(sock: &UdpSocket, e: Event, addr: SocketAddr) -> Result<usize> {
log::trace!("{:20} ------>->->-> {addr}", e.to_string());
let data: Vec<u8> = (&e).into();
// When udp blocks, we dont want to block the event loop.
// Dropping events is better than potentially crashing the input capture.
Ok(sock.try_send_to(&data, addr)?)
}

View File

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

View File

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