mirror of
https://github.com/feschber/lan-mouse.git
synced 2026-03-07 20:09:59 +03:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66bce9083e | ||
|
|
102b64f2b4 | ||
|
|
4b499742ad | ||
|
|
a86d74b52c | ||
|
|
c25a15e2d8 | ||
|
|
8b82325bdb | ||
|
|
5415205c83 | ||
|
|
1666fb8b7b | ||
|
|
9afe7da0dd | ||
|
|
f7c59e40c9 | ||
|
|
5be5b0ad7c | ||
|
|
8ed4520172 | ||
|
|
9e56c546cd | ||
|
|
daf8818a9f | ||
|
|
6eaa199503 | ||
|
|
8ff991aefe | ||
|
|
abf95afb9f | ||
|
|
9a75a7622e | ||
|
|
0196cfe56c | ||
|
|
097468f708 | ||
|
|
3470abc03a | ||
|
|
a7397ad4f4 | ||
|
|
9889b49f10 | ||
|
|
f4db2366b7 | ||
|
|
c9deb6eba4 | ||
|
|
5cc8cda19d | ||
|
|
8084b52cfc | ||
|
|
1f4821a16d | ||
|
|
82926d8272 | ||
|
|
006831b9f1 | ||
|
|
e5b770a799 | ||
|
|
017bc43176 | ||
|
|
36001c6fb2 | ||
|
|
2803db7073 | ||
|
|
622b04b36c | ||
|
|
61ff05c95a | ||
|
|
ecab3a360d | ||
|
|
6674af8e63 | ||
|
|
b3caba99ab | ||
|
|
fad48c2504 | ||
|
|
f28f75418c | ||
|
|
e2c47d3096 | ||
|
|
f19944515a | ||
|
|
535cd055b9 | ||
|
|
118c0dfc73 | ||
|
|
7897db6047 | ||
|
|
347256e966 | ||
|
|
0017dbc634 | ||
|
|
d90eb0cd0f | ||
|
|
2e52660714 | ||
|
|
8c276f88b7 | ||
|
|
13597b3587 | ||
|
|
b59808742a |
24
.github/workflows/cachix.yml
vendored
Normal file
24
.github/workflows/cachix.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
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
|
||||
14
.github/workflows/pre-release.yml
vendored
14
.github/workflows/pre-release.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
linux-release-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
- name: Release Build
|
||||
run: cargo build --release
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lan-mouse-linux
|
||||
path: target/release/lan-mouse
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
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\notbin" "C:\Program Files\Git\bin"
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Release Build
|
||||
run: cargo build --release
|
||||
- name: Create Archive
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
Copy-Item -Path "target\release\lan-mouse.exe" -Destination "lan-mouse-windows"
|
||||
Compress-Archive -Path "lan-mouse-windows\*" -DestinationPath lan-mouse-windows.zip
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lan-mouse-windows
|
||||
path: lan-mouse-windows.zip
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
macos-release-build:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: install dependencies
|
||||
run: brew install gtk4 libadwaita
|
||||
- name: Release Build
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
cargo build --release
|
||||
cp target/release/lan-mouse lan-mouse-macos-intel
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lan-mouse-macos
|
||||
path: lan-mouse-macos-intel
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
- name: Create Release
|
||||
uses: "marvinpinto/action-automatic-releases@latest"
|
||||
with:
|
||||
|
||||
12
.github/workflows/rust.yml
vendored
12
.github/workflows/rust.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
- name: Clippy
|
||||
run: cargo clippy --all-features --all-targets -- --deny warnings
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lan-mouse
|
||||
path: target/debug/lan-mouse
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
- name: Copy Gtk Dlls
|
||||
run: Get-Childitem -Path "C:\\gtk-build\\gtk\\x64\\release\\bin\\*.dll" -File -Recurse | Copy-Item -Destination "target\\debug"
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lan-mouse-windows
|
||||
path: |
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
build-macos:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: install dependencies
|
||||
run: brew install gtk4 libadwaita
|
||||
- name: Build
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
- name: Clippy
|
||||
run: cargo clippy --all-features --all-targets -- --deny warnings
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lan-mouse-macos
|
||||
path: target/debug/lan-mouse
|
||||
|
||||
14
.github/workflows/tagged-release.yml
vendored
14
.github/workflows/tagged-release.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
linux-release-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
- name: Release Build
|
||||
run: cargo build --release
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lan-mouse-linux
|
||||
path: target/release/lan-mouse
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
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\notbin" "C:\Program Files\Git\bin"
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Release Build
|
||||
run: cargo build --release
|
||||
- name: Create Archive
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
Copy-Item -Path "target\release\lan-mouse.exe" -Destination "lan-mouse-windows"
|
||||
Compress-Archive -Path "lan-mouse-windows\*" -DestinationPath lan-mouse-windows.zip
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lan-mouse-windows
|
||||
path: lan-mouse-windows.zip
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
macos-release-build:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: install dependencies
|
||||
run: brew install gtk4 libadwaita
|
||||
- name: Release Build
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
cargo build --release
|
||||
cp target/release/lan-mouse lan-mouse-macos-intel
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lan-mouse-macos
|
||||
path: lan-mouse-macos-intel
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
- name: "Create Release"
|
||||
uses: "marvinpinto/action-automatic-releases@latest"
|
||||
with:
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,4 +1,7 @@
|
||||
/target
|
||||
.gdbinit
|
||||
.idea/
|
||||
.vs/
|
||||
.vs/
|
||||
.vscode/
|
||||
.direnv/
|
||||
result
|
||||
976
Cargo.lock
generated
976
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
16
Cargo.toml
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "lan-mouse"
|
||||
description = "Software KVM Switch / mouse & keyboard sharing software for Local Area Networks"
|
||||
version = "0.5.1"
|
||||
version = "0.7.2"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
repository = "https://github.com/ferdinandschober/lan-mouse"
|
||||
@@ -20,16 +20,18 @@ toml = "0.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
anyhow = "1.0.71"
|
||||
log = "0.4.20"
|
||||
env_logger = "0.10.0"
|
||||
env_logger = "0.11.3"
|
||||
serde_json = "1.0.107"
|
||||
tokio = {version = "1.32.0", features = ["io-util", "macros", "net", "rt", "sync", "signal"] }
|
||||
async-trait = "0.1.73"
|
||||
futures-core = "0.3.28"
|
||||
futures = "0.3.28"
|
||||
clap = { version="4.4.11", features = ["derive"] }
|
||||
gtk = { package = "gtk4", version = "0.7.2", features = ["v4_2"], optional = true }
|
||||
adw = { package = "libadwaita", version = "0.5.2", features = ["v1_1"], optional = true }
|
||||
gtk = { package = "gtk4", version = "0.8.1", features = ["v4_2"], optional = true }
|
||||
adw = { package = "libadwaita", version = "0.6.0", features = ["v1_1"], optional = true }
|
||||
async-channel = { version = "2.1.1", optional = true }
|
||||
keycode = "0.4.0"
|
||||
once_cell = "1.19.0"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2.148"
|
||||
@@ -40,8 +42,8 @@ wayland-protocols = { version="0.31.0", features=["client", "staging", "unstable
|
||||
wayland-protocols-wlr = { version="0.2.0", features=["client"], optional = true }
|
||||
wayland-protocols-misc = { version="0.2.0", features=["client"], optional = true }
|
||||
x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
|
||||
ashpd = { version = "0.6.2", default-features = false, features = ["tokio"], optional = true }
|
||||
reis = { git = "https://github.com/ids1024/reis", features = [ "tokio" ], optional = true }
|
||||
ashpd = { version = "0.8", default-features = false, features = ["tokio"], optional = true }
|
||||
reis = { version = "0.2", features = [ "tokio" ], optional = true }
|
||||
|
||||
[target.'cfg(target_os="macos")'.dependencies]
|
||||
core-graphics = { version = "0.23", features = ["highsierra"] }
|
||||
@@ -50,7 +52,7 @@ core-graphics = { version = "0.23", features = ["highsierra"] }
|
||||
winapi = { version = "0.3.9", features = ["winuser"] }
|
||||
|
||||
[build-dependencies]
|
||||
glib-build-tools = "0.18.0"
|
||||
glib-build-tools = "0.19.0"
|
||||
|
||||
[features]
|
||||
default = ["wayland", "x11", "xdg_desktop_portal", "libei", "gtk"]
|
||||
|
||||
210
README.md
210
README.md
@@ -43,84 +43,44 @@ input capture (to send events *to* other clients) on different operating systems
|
||||
|---------------------------|--------------------------|--------------------------------------|
|
||||
| Wayland (wlroots) | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| Wayland (KDE) | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| Wayland (Gnome) | :heavy_check_mark: | WIP |
|
||||
| Wayland (Gnome) | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| X11 | :heavy_check_mark: | WIP |
|
||||
| Windows | :heavy_check_mark: | WIP |
|
||||
| MacOS | ( :heavy_check_mark: ) | WIP |
|
||||
|
||||
Keycode translation is not yet implemented so on MacOS only mouse emulation works as of right now.
|
||||
| MacOS | :heavy_check_mark: | WIP |
|
||||
|
||||
> [!Important]
|
||||
> Gnome -> Sway only partially works (modifier events are not handled correctly)
|
||||
|
||||
> [!Important]
|
||||
> **Wayfire**
|
||||
>
|
||||
> If you are using [Wayfire](https://github.com/WayfireWM/wayfire), make sure to use a recent version (must be newer than October 23rd) and **add `shortcuts-inhibit` to the list of plugins in your wayfire config!**
|
||||
> Otherwise input capture will not work.
|
||||
|
||||
## Build and Run
|
||||
## Installation
|
||||
|
||||
### Install Dependencies
|
||||
#### Macos
|
||||
### 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
|
||||
brew install libadwaita
|
||||
# git version (includes latest changes)
|
||||
paru -S lan-mouse-git
|
||||
|
||||
# alternatively
|
||||
paru -S lan-mouse-bin
|
||||
```
|
||||
|
||||
#### Ubuntu and derivatives
|
||||
```sh
|
||||
sudo apt install libadwaita-1-dev libgtk-4-dev libx11-dev libxtst-dev
|
||||
```
|
||||
### 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)
|
||||
|
||||
#### Arch and derivatives
|
||||
```sh
|
||||
sudo pacman -S libadwaita gtk libx11 libxtst
|
||||
```
|
||||
|
||||
#### Fedora and derivatives
|
||||
```sh
|
||||
sudo dnf install libadwaita-devel libXtst-devel libX11-devel
|
||||
```
|
||||
### Building from Source
|
||||
|
||||
#### Windows
|
||||
Follow the instructions at [gtk-rs.org](https://gtk-rs.org/gtk4-rs/stable/latest/book/installation_windows.html)
|
||||
|
||||
*TLDR:*
|
||||
|
||||
Build gtk from source
|
||||
|
||||
- The following commands should be run in an admin power shell instance:
|
||||
```sh
|
||||
# install chocolatey
|
||||
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)
|
||||
choco install python --version=3.11.0
|
||||
|
||||
# install git
|
||||
choco install git
|
||||
|
||||
# install msys2
|
||||
choco install msys2
|
||||
|
||||
# install Visual Studio 2022
|
||||
choco install visualstudio2022-workload-vctools
|
||||
```
|
||||
|
||||
- The following commands should be run in a regular power shell instance:
|
||||
|
||||
```sh
|
||||
# install gvsbuild with python
|
||||
python -m pip install --user pipx
|
||||
python -m pipx ensurepath
|
||||
pipx install gvsbuild
|
||||
|
||||
# build gtk + libadwaita
|
||||
gvsbuild build gtk4 libadwaita librsvg
|
||||
```
|
||||
|
||||
Make sure to add the directory `C:\gtk-build\gtk\x64\release\bin`
|
||||
[to the `PATH` environment variable]((https://learn.microsoft.com/en-us/previous-versions/office/developer/sharepoint-2010/ee537574(v=office.14))). Otherwise the project will fail to build.
|
||||
|
||||
To avoid building GTK from source, it is possible to disable
|
||||
the gtk frontend (see conditional compilation below).
|
||||
|
||||
### Build and run
|
||||
Build in release mode:
|
||||
```sh
|
||||
cargo build --release
|
||||
@@ -131,6 +91,27 @@ Run directly:
|
||||
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.
|
||||
@@ -151,6 +132,79 @@ cargo build --no-default-features --features wayland
|
||||
```
|
||||
For a detailed list of available features, checkout the [Cargo.toml](./Cargo.toml)
|
||||
|
||||
|
||||
## Installing Dependencies
|
||||
|
||||
#### Macos
|
||||
```sh
|
||||
brew install libadwaita
|
||||
```
|
||||
|
||||
#### Ubuntu and derivatives
|
||||
```sh
|
||||
sudo apt install libadwaita-1-dev libgtk-4-dev libx11-dev libxtst-dev
|
||||
```
|
||||
|
||||
#### Arch and derivatives
|
||||
```sh
|
||||
sudo pacman -S libadwaita gtk libx11 libxtst
|
||||
```
|
||||
|
||||
#### Fedora and derivatives
|
||||
```sh
|
||||
sudo dnf install libadwaita-devel libXtst-devel libX11-devel
|
||||
```
|
||||
|
||||
#### 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)
|
||||
|
||||
*TLDR:*
|
||||
|
||||
Build gtk from source
|
||||
|
||||
- The following commands should be run in an admin power shell instance:
|
||||
```sh
|
||||
# install chocolatey
|
||||
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
|
||||
choco install python --version=3.11.0
|
||||
|
||||
# install git
|
||||
choco install git
|
||||
|
||||
# install msys2
|
||||
choco install msys2
|
||||
|
||||
# install Visual Studio 2022
|
||||
choco install visualstudio2022-workload-vctools
|
||||
```
|
||||
|
||||
- The following commands should be run in a regular power shell instance:
|
||||
|
||||
```sh
|
||||
# install gvsbuild with python
|
||||
python -m pip install --user pipx
|
||||
python -m pipx ensurepath
|
||||
```
|
||||
|
||||
- Relaunch your powershell instance so the changes in the environment are reflected.
|
||||
```sh
|
||||
pipx install gvsbuild
|
||||
|
||||
# build gtk + libadwaita
|
||||
gvsbuild build gtk4 libadwaita librsvg
|
||||
```
|
||||
|
||||
Make sure to add the directory `C:\gtk-build\gtk\x64\release\bin`
|
||||
[to the `PATH` environment variable]((https://learn.microsoft.com/en-us/previous-versions/office/developer/sharepoint-2010/ee537574(v=office.14))). Otherwise the project will fail to build.
|
||||
|
||||
To avoid building GTK from source, it is possible to disable
|
||||
the gtk frontend (see conditional compilation below).
|
||||
|
||||
## Usage
|
||||
### Gtk Frontend
|
||||
By default the gtk frontend will open when running `lan-mouse`.
|
||||
@@ -184,6 +238,17 @@ To do so, add `--daemon` to the commandline args:
|
||||
$ 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
|
||||
To automatically load clients on startup, the file `$XDG_CONFIG_HOME/lan-mouse/config.toml` is parsed.
|
||||
`$XDG_CONFIG_HOME` defaults to `~/.config/`.
|
||||
@@ -191,10 +256,17 @@ 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:
|
||||
|
||||
### 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
|
||||
# example configuration
|
||||
|
||||
# configure release bind
|
||||
release_bind = [ "KeyA", "KeyS", "KeyD", "KeyF" ]
|
||||
|
||||
# optional port (defaults to 4242)
|
||||
port = 4242
|
||||
# # optional frontend -> defaults to gtk if available
|
||||
@@ -204,7 +276,9 @@ port = 4242
|
||||
# define a client on the right side with host name "iridium"
|
||||
[right]
|
||||
# hostname
|
||||
host_name = "iridium"
|
||||
hostname = "iridium"
|
||||
# activate this client immediately when lan-mouse is started
|
||||
activate_on_startup = true
|
||||
# optional list of (known) ip addresses
|
||||
ips = ["192.168.178.156"]
|
||||
|
||||
@@ -212,7 +286,7 @@ ips = ["192.168.178.156"]
|
||||
[left]
|
||||
# The hostname is optional: When no hostname is specified,
|
||||
# at least one ip address needs to be specified.
|
||||
host_name = "thorium"
|
||||
hostname = "thorium"
|
||||
# ips for ethernet and wifi
|
||||
ips = ["192.168.178.189", "192.168.178.172"]
|
||||
# optional port
|
||||
@@ -227,11 +301,11 @@ Where `left` can be either `left`, `right`, `top` or `bottom`.
|
||||
- [x] IP Address switching
|
||||
- [x] Liveness tracking Automatically ungrab mouse when client unreachable
|
||||
- [x] Liveness tracking: Automatically release keys, when server offline
|
||||
- [ ] Libei Input Capture
|
||||
- [x] MacOS KeyCode Translation
|
||||
- [x] Libei Input Capture
|
||||
- [ ] X11 Input Capture
|
||||
- [ ] Windows Input Capture
|
||||
- [ ] MacOS Input Capture
|
||||
- [ ] MacOS KeyCode Translation
|
||||
- [ ] Latency measurement and visualization
|
||||
- [ ] Bandwidth usage measurement and visualization
|
||||
- [ ] Clipboard support
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# example configuration
|
||||
|
||||
# release bind
|
||||
release_bind = [ "KeyA", "KeyS", "KeyD", "KeyF" ]
|
||||
|
||||
# optional port (defaults to 4242)
|
||||
port = 4242
|
||||
# optional frontend -> defaults to gtk if available
|
||||
@@ -8,7 +11,7 @@ port = 4242
|
||||
# define a client on the right side with host name "iridium"
|
||||
[right]
|
||||
# hostname
|
||||
host_name = "iridium"
|
||||
hostname = "iridium"
|
||||
# optional list of (known) ip addresses
|
||||
ips = ["192.168.178.156"]
|
||||
|
||||
@@ -16,7 +19,7 @@ ips = ["192.168.178.156"]
|
||||
[left]
|
||||
# The hostname is optional: When no hostname is specified,
|
||||
# at least one ip address needs to be specified.
|
||||
host_name = "thorium"
|
||||
hostname = "thorium"
|
||||
# ips for ethernet and wifi
|
||||
ips = ["192.168.178.189", "192.168.178.172"]
|
||||
# optional port
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
[Desktop Entry]
|
||||
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
|
||||
Exec=lan-mouse
|
||||
Icon=mouse-icon.svg
|
||||
Icon=de.feschber.LanMouse
|
||||
Name[en_US]=Lan Mouse
|
||||
Name[de_DE]=Lan Maus
|
||||
Name=Lan Mouse
|
||||
StartupNotify=true
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Version=0.5.1
|
||||
8
firewall/lan-mouse.xml
Normal file
8
firewall/lan-mouse.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?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
Normal file
82
flake.lock
generated
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
58
flake.nix
Normal file
58
flake.nix
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
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";
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
40
nix/README.md
Normal file
40
nix/README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# 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
|
||||
};
|
||||
}
|
||||
|
||||
```
|
||||
42
nix/default.nix
Normal file
42
nix/default.nix
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
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;
|
||||
};
|
||||
}
|
||||
50
nix/hm-module.nix
Normal file
50
nix/hm-module.nix
Normal file
@@ -0,0 +1,50 @@
|
||||
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
|
||||
];
|
||||
};
|
||||
}
|
||||
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
@@ -7,6 +7,6 @@
|
||||
<file compressed="true">style-dark.css</file>
|
||||
</gresource>
|
||||
<gresource prefix="/de/feschber/LanMouse/icons">
|
||||
<file compressed="true" preprocess="xml-stripblanks">mouse-icon.svg</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">de.feschber.LanMouse.svg</file>
|
||||
</gresource>
|
||||
</gresources>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<object class="AdwStatusPage">
|
||||
<property name="title" translatable="yes">Lan Mouse</property>
|
||||
<property name="description" translatable="yes">easily use your mouse and keyboard on multiple computers</property>
|
||||
<property name="icon-name">mouse-icon</property>
|
||||
<property name="icon-name">de.feschber.LanMouse</property>
|
||||
<property name="child">
|
||||
<object class="AdwClamp">
|
||||
<property name="maximum-size">600</property>
|
||||
|
||||
13
service/lan-mouse.service
Normal file
13
service/lan-mouse.service
Normal file
@@ -0,0 +1,13 @@
|
||||
[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
|
||||
1
shell.nix
Normal file
1
shell.nix
Normal file
@@ -0,0 +1 @@
|
||||
(builtins.getFlake ("git+file://" + toString ./.)).devShells.${builtins.currentSystem}.default
|
||||
@@ -1,15 +1,17 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io,
|
||||
os::{
|
||||
fd::{FromRawFd, RawFd},
|
||||
unix::net::UnixStream,
|
||||
},
|
||||
os::{fd::OwnedFd, unix::net::UnixStream},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use ashpd::desktop::remote_desktop::{DeviceType, RemoteDesktop};
|
||||
use ashpd::{
|
||||
desktop::{
|
||||
remote_desktop::{DeviceType, RemoteDesktop},
|
||||
ResponseError,
|
||||
},
|
||||
WindowIdentifier,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
|
||||
@@ -43,22 +45,34 @@ pub struct LibeiConsumer {
|
||||
serial: u32,
|
||||
}
|
||||
|
||||
async fn get_ei_fd() -> Result<RawFd, ashpd::Error> {
|
||||
async fn get_ei_fd() -> Result<OwnedFd, ashpd::Error> {
|
||||
let proxy = RemoteDesktop::new().await?;
|
||||
let session = proxy.create_session().await?;
|
||||
|
||||
// I HATE EVERYTHING, THIS TOOK 8 HOURS OF DEBUGGING
|
||||
proxy
|
||||
.select_devices(
|
||||
&session,
|
||||
DeviceType::Pointer | DeviceType::Keyboard | DeviceType::Touchscreen,
|
||||
)
|
||||
.await?;
|
||||
// retry when user presses the cancel button
|
||||
let (session, _) = loop {
|
||||
log::debug!("creating session ...");
|
||||
let session = proxy.create_session().await?;
|
||||
|
||||
log::debug!("selecting devices ...");
|
||||
proxy
|
||||
.select_devices(&session, DeviceType::Keyboard | DeviceType::Pointer)
|
||||
.await?;
|
||||
|
||||
log::info!("requesting permission for input emulation");
|
||||
match proxy
|
||||
.start(&session, &WindowIdentifier::default())
|
||||
.await?
|
||||
.response()
|
||||
{
|
||||
Ok(d) => break (session, d),
|
||||
Err(ashpd::Error::Response(ResponseError::Cancelled)) => {
|
||||
log::warn!("request cancelled!");
|
||||
continue;
|
||||
}
|
||||
e => e?,
|
||||
};
|
||||
};
|
||||
|
||||
proxy
|
||||
.start(&session, &ashpd::WindowIdentifier::default())
|
||||
.await?
|
||||
.response()?;
|
||||
proxy.connect_to_eis(&session).await
|
||||
}
|
||||
|
||||
@@ -66,15 +80,7 @@ impl LibeiConsumer {
|
||||
pub async fn new() -> Result<Self> {
|
||||
// fd is owned by the message, so we need to dup it
|
||||
let eifd = get_ei_fd().await?;
|
||||
let 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::from(eifd);
|
||||
// let stream = UnixStream::connect("/run/user/1000/eis-0")?;
|
||||
stream.set_nonblocking(true)?;
|
||||
let context = ei::Context::new(stream)?;
|
||||
|
||||
@@ -5,13 +5,20 @@ use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use core_graphics::display::{CGDisplayBounds, CGMainDisplayID, CGPoint};
|
||||
use core_graphics::event::{
|
||||
CGEvent, CGEventTapLocation, CGEventType, CGMouseButton, EventField, ScrollEventUnit,
|
||||
CGEvent, CGEventTapLocation, CGEventType, CGKeyCode, CGMouseButton, EventField, ScrollEventUnit,
|
||||
};
|
||||
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
|
||||
use keycode::{KeyMap, KeyMapping};
|
||||
use std::ops::{Index, IndexMut};
|
||||
use std::time::Duration;
|
||||
use tokio::task::AbortHandle;
|
||||
|
||||
const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500);
|
||||
const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32);
|
||||
|
||||
pub struct MacOSConsumer {
|
||||
pub event_source: CGEventSource,
|
||||
repeat_task: Option<AbortHandle>,
|
||||
button_state: ButtonState,
|
||||
}
|
||||
|
||||
@@ -59,6 +66,7 @@ impl MacOSConsumer {
|
||||
Ok(Self {
|
||||
event_source,
|
||||
button_state,
|
||||
repeat_task: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -66,6 +74,37 @@ impl MacOSConsumer {
|
||||
let event: CGEvent = CGEvent::new(self.event_source.clone()).ok()?;
|
||||
Some(event.location())
|
||||
}
|
||||
|
||||
async fn spawn_repeat_task(&mut self, key: u16) {
|
||||
// there can only be one repeating key and it's
|
||||
// always the last to be pressed
|
||||
self.kill_repeat_task();
|
||||
let event_source = self.event_source.clone();
|
||||
let repeat_task = tokio::task::spawn_local(async move {
|
||||
tokio::time::sleep(DEFAULT_REPEAT_DELAY).await;
|
||||
loop {
|
||||
key_event(event_source.clone(), key, 1);
|
||||
tokio::time::sleep(DEFAULT_REPEAT_INTERVAL).await;
|
||||
}
|
||||
});
|
||||
self.repeat_task = Some(repeat_task.abort_handle());
|
||||
}
|
||||
fn kill_repeat_task(&mut self) {
|
||||
if let Some(task) = self.repeat_task.take() {
|
||||
task.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn key_event(event_source: CGEventSource, key: u16, state: u8) {
|
||||
let event = match CGEvent::new_keyboard_event(event_source, key, state != 0) {
|
||||
Ok(e) => e,
|
||||
Err(_) => {
|
||||
log::warn!("unable to create key event");
|
||||
return;
|
||||
}
|
||||
};
|
||||
event.post(CGEventTapLocation::HID);
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -209,22 +248,24 @@ impl EventConsumer for MacOSConsumer {
|
||||
PointerEvent::Frame { .. } => {}
|
||||
},
|
||||
Event::Keyboard(keyboard_event) => match keyboard_event {
|
||||
KeyboardEvent::Key { .. } => {
|
||||
/*
|
||||
let code = CGKeyCode::from_le(key as u16);
|
||||
let event = match CGEvent::new_keyboard_event(
|
||||
self.event_source.clone(),
|
||||
code,
|
||||
match state { 1 => true, _ => false }
|
||||
) {
|
||||
Ok(e) => e,
|
||||
KeyboardEvent::Key {
|
||||
time: _,
|
||||
key,
|
||||
state,
|
||||
} => {
|
||||
let code = match KeyMap::from_key_mapping(KeyMapping::Evdev(key as u16)) {
|
||||
Ok(k) => k.mac as CGKeyCode,
|
||||
Err(_) => {
|
||||
log::warn!("unable to create key event");
|
||||
return
|
||||
log::warn!("unable to map key event");
|
||||
return;
|
||||
}
|
||||
};
|
||||
event.post(CGEventTapLocation::HID);
|
||||
*/
|
||||
match state {
|
||||
// pressed
|
||||
1 => self.spawn_repeat_task(code).await,
|
||||
_ => self.kill_repeat_task(),
|
||||
}
|
||||
key_event(self.event_source.clone(), code, state)
|
||||
}
|
||||
KeyboardEvent::Modifiers { .. } => {}
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ use anyhow::Result;
|
||||
use ashpd::{
|
||||
desktop::{
|
||||
remote_desktop::{Axis, DeviceType, KeyState, RemoteDesktop},
|
||||
Session,
|
||||
ResponseError, Session,
|
||||
},
|
||||
WindowIdentifier,
|
||||
};
|
||||
@@ -26,17 +26,32 @@ impl<'a> DesktopPortalConsumer<'a> {
|
||||
pub async fn new() -> Result<DesktopPortalConsumer<'a>> {
|
||||
log::debug!("connecting to org.freedesktop.portal.RemoteDesktop portal ...");
|
||||
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?;
|
||||
|
||||
let _ = proxy
|
||||
.start(&session, &WindowIdentifier::default())
|
||||
.await?
|
||||
.response()?;
|
||||
// retry when user presses the cancel button
|
||||
let (session, _) = loop {
|
||||
log::debug!("creating session ...");
|
||||
let session = proxy.create_session().await?;
|
||||
|
||||
log::debug!("selecting devices ...");
|
||||
proxy
|
||||
.select_devices(&session, DeviceType::Keyboard | DeviceType::Pointer)
|
||||
.await?;
|
||||
|
||||
log::info!("requesting permission for input emulation");
|
||||
match proxy
|
||||
.start(&session, &WindowIdentifier::default())
|
||||
.await?
|
||||
.response()
|
||||
{
|
||||
Ok(d) => break (session, d),
|
||||
Err(ashpd::Error::Response(ResponseError::Cancelled)) => {
|
||||
log::warn!("request cancelled!");
|
||||
continue;
|
||||
}
|
||||
e => e?,
|
||||
};
|
||||
};
|
||||
|
||||
log::debug!("started session");
|
||||
|
||||
Ok(Self { proxy, session })
|
||||
|
||||
@@ -1,39 +1,555 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::{io, task::Poll};
|
||||
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::{
|
||||
client::{ClientEvent, ClientHandle},
|
||||
event::Event,
|
||||
client::{ClientEvent, ClientHandle, Position},
|
||||
event::{Event, KeyboardEvent, PointerEvent},
|
||||
producer::EventProducer,
|
||||
};
|
||||
|
||||
pub struct LibeiProducer {}
|
||||
#[derive(Debug)]
|
||||
enum ProducerEvent {
|
||||
Release,
|
||||
ClientEvent(ClientEvent),
|
||||
}
|
||||
|
||||
impl LibeiProducer {
|
||||
pub fn new() -> Result<Self> {
|
||||
Err(anyhow!("not implemented"))
|
||||
#[allow(dead_code)]
|
||||
pub struct LibeiProducer<'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),
|
||||
}
|
||||
}
|
||||
|
||||
impl EventProducer for LibeiProducer {
|
||||
fn notify(&mut self, _event: ClientEvent) -> io::Result<()> {
|
||||
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 LibeiProducer<'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> LibeiProducer<'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> EventProducer for LibeiProducer<'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 Stream for LibeiProducer {
|
||||
impl<'a> Stream for LibeiProducer<'a> {
|
||||
type Item = io::Result<(ClientHandle, Event)>;
|
||||
|
||||
fn poll_next(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Option<Self::Item>> {
|
||||
Poll::Pending
|
||||
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))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,9 +54,12 @@ use wayland_client::{
|
||||
delegate_noop,
|
||||
globals::{registry_queue_init, GlobalListContents},
|
||||
protocol::{
|
||||
wl_buffer, wl_compositor, wl_keyboard,
|
||||
wl_buffer, wl_compositor,
|
||||
wl_keyboard::{self, WlKeyboard},
|
||||
wl_output::{self, WlOutput},
|
||||
wl_pointer, wl_region, wl_registry, wl_seat, wl_shm, wl_shm_pool, wl_surface,
|
||||
wl_pointer::{self, WlPointer},
|
||||
wl_region, wl_registry, wl_seat, wl_shm, wl_shm_pool,
|
||||
wl_surface::WlSurface,
|
||||
},
|
||||
Connection, Dispatch, DispatchError, EventQueue, QueueHandle, WEnum,
|
||||
};
|
||||
@@ -73,7 +76,7 @@ struct Globals {
|
||||
seat: wl_seat::WlSeat,
|
||||
shm: wl_shm::WlShm,
|
||||
layer_shell: ZwlrLayerShellV1,
|
||||
outputs: Vec<wl_output::WlOutput>,
|
||||
outputs: Vec<WlOutput>,
|
||||
xdg_output_manager: ZxdgOutputManagerV1,
|
||||
}
|
||||
|
||||
@@ -95,6 +98,8 @@ impl OutputInfo {
|
||||
}
|
||||
|
||||
struct State {
|
||||
pointer: Option<WlPointer>,
|
||||
keyboard: Option<WlKeyboard>,
|
||||
pointer_lock: Option<ZwpLockedPointerV1>,
|
||||
rel_pointer: Option<ZwpRelativePointerV1>,
|
||||
shortcut_inhibitor: Option<ZwpKeyboardShortcutsInhibitorV1>,
|
||||
@@ -123,7 +128,7 @@ pub struct WaylandEventProducer(AsyncFd<Inner>);
|
||||
|
||||
struct Window {
|
||||
buffer: wl_buffer::WlBuffer,
|
||||
surface: wl_surface::WlSurface,
|
||||
surface: WlSurface,
|
||||
layer_surface: ZwlrLayerSurfaceV1,
|
||||
pos: Position,
|
||||
}
|
||||
@@ -136,6 +141,7 @@ impl Window {
|
||||
pos: Position,
|
||||
size: (i32, i32),
|
||||
) -> Window {
|
||||
log::debug!("creating window output: {output:?}, size: {size:?}");
|
||||
let g = &state.g;
|
||||
|
||||
let (width, height) = match pos {
|
||||
@@ -217,6 +223,7 @@ fn get_edges(outputs: &[(WlOutput, OutputInfo)], pos: Position) -> Vec<(WlOutput
|
||||
fn get_output_configuration(state: &State, pos: Position) -> Vec<(WlOutput, OutputInfo)> {
|
||||
// get all output edges corresponding to the position
|
||||
let edges = get_edges(&state.output_info, pos);
|
||||
log::debug!("edges: {edges:?}");
|
||||
let opposite_edges = get_edges(&state.output_info, pos.opposite());
|
||||
|
||||
// remove those edges that are at the same position
|
||||
@@ -331,6 +338,8 @@ impl WaylandEventProducer {
|
||||
std::mem::drop(read_guard);
|
||||
|
||||
let mut state = State {
|
||||
pointer: None,
|
||||
keyboard: None,
|
||||
g,
|
||||
pointer_lock: None,
|
||||
rel_pointer: None,
|
||||
@@ -406,8 +415,8 @@ impl WaylandEventProducer {
|
||||
impl State {
|
||||
fn grab(
|
||||
&mut self,
|
||||
surface: &wl_surface::WlSurface,
|
||||
pointer: &wl_pointer::WlPointer,
|
||||
surface: &WlSurface,
|
||||
pointer: &WlPointer,
|
||||
serial: u32,
|
||||
qh: &QueueHandle<State>,
|
||||
) {
|
||||
@@ -489,6 +498,7 @@ impl State {
|
||||
fn add_client(&mut self, client: ClientHandle, pos: Position) {
|
||||
let outputs = get_output_configuration(self, pos);
|
||||
|
||||
log::debug!("outputs: {outputs:?}");
|
||||
outputs.iter().for_each(|(o, i)| {
|
||||
let window = Window::new(self, &self.qh, o, pos, i.size);
|
||||
let window = Rc::new(window);
|
||||
@@ -654,7 +664,7 @@ impl Stream for WaylandEventProducer {
|
||||
|
||||
impl Dispatch<wl_seat::WlSeat, ()> for State {
|
||||
fn event(
|
||||
_: &mut Self,
|
||||
state: &mut Self,
|
||||
seat: &wl_seat::WlSeat,
|
||||
event: <wl_seat::WlSeat as wayland_client::Proxy>::Event,
|
||||
_: &(),
|
||||
@@ -665,21 +675,21 @@ impl Dispatch<wl_seat::WlSeat, ()> for State {
|
||||
capabilities: WEnum::Value(capabilities),
|
||||
} = event
|
||||
{
|
||||
if capabilities.contains(wl_seat::Capability::Pointer) {
|
||||
seat.get_pointer(qh, ());
|
||||
if capabilities.contains(wl_seat::Capability::Pointer) && state.pointer.is_none() {
|
||||
state.pointer.replace(seat.get_pointer(qh, ()));
|
||||
}
|
||||
if capabilities.contains(wl_seat::Capability::Keyboard) {
|
||||
if capabilities.contains(wl_seat::Capability::Keyboard) && state.keyboard.is_none() {
|
||||
seat.get_keyboard(qh, ());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<wl_pointer::WlPointer, ()> for State {
|
||||
impl Dispatch<WlPointer, ()> for State {
|
||||
fn event(
|
||||
app: &mut Self,
|
||||
pointer: &wl_pointer::WlPointer,
|
||||
event: <wl_pointer::WlPointer as wayland_client::Proxy>::Event,
|
||||
pointer: &WlPointer,
|
||||
event: <WlPointer as wayland_client::Proxy>::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
@@ -719,6 +729,9 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
|
||||
* 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();
|
||||
}
|
||||
wl_pointer::Event::Button {
|
||||
@@ -758,10 +771,10 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<wl_keyboard::WlKeyboard, ()> for State {
|
||||
impl Dispatch<WlKeyboard, ()> for State {
|
||||
fn event(
|
||||
app: &mut Self,
|
||||
_: &wl_keyboard::WlKeyboard,
|
||||
_: &WlKeyboard,
|
||||
event: wl_keyboard::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
@@ -914,7 +927,7 @@ impl Dispatch<wl_registry::WlRegistry, ()> for State {
|
||||
state
|
||||
.g
|
||||
.outputs
|
||||
.push(registry.bind::<wl_output::WlOutput, _, _>(name, 4, qh, ()))
|
||||
.push(registry.bind::<WlOutput, _, _>(name, 4, qh, ()))
|
||||
}
|
||||
}
|
||||
wl_registry::Event::GlobalRemove { .. } => {}
|
||||
@@ -959,11 +972,11 @@ impl Dispatch<ZxdgOutputV1, WlOutput> for State {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<wl_output::WlOutput, ()> for State {
|
||||
impl Dispatch<WlOutput, ()> for State {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_proxy: &wl_output::WlOutput,
|
||||
event: <wl_output::WlOutput as wayland_client::Proxy>::Event,
|
||||
_proxy: &WlOutput,
|
||||
event: <WlOutput as wayland_client::Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
@@ -987,6 +1000,6 @@ delegate_noop!(State: ZwpPointerConstraintsV1);
|
||||
delegate_noop!(State: ignore ZxdgOutputManagerV1);
|
||||
delegate_noop!(State: ignore wl_shm::WlShm);
|
||||
delegate_noop!(State: ignore wl_buffer::WlBuffer);
|
||||
delegate_noop!(State: ignore wl_surface::WlSurface);
|
||||
delegate_noop!(State: ignore WlSurface);
|
||||
delegate_noop!(State: ignore ZwpKeyboardShortcutsInhibitorV1);
|
||||
delegate_noop!(State: ignore ZwpLockedPointerV1);
|
||||
|
||||
@@ -46,6 +46,20 @@ 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)]
|
||||
pub struct Client {
|
||||
/// hostname of this client
|
||||
@@ -56,10 +70,10 @@ pub struct Client {
|
||||
/// This way any event consumer / producer backend does not
|
||||
/// need to know anything about a client other than its handle.
|
||||
pub handle: ClientHandle,
|
||||
/// all socket addresses associated with a particular client
|
||||
/// all ip addresses associated with a particular client
|
||||
/// e.g. Laptops usually have at least an ethernet and a wifi port
|
||||
/// which have different ip addresses
|
||||
pub addrs: HashSet<SocketAddr>,
|
||||
pub ips: HashSet<IpAddr>,
|
||||
/// both active_addr and addrs can be None / empty so port needs to be stored seperately
|
||||
pub port: u16,
|
||||
/// position of a client on screen
|
||||
@@ -112,6 +126,7 @@ impl ClientManager {
|
||||
ips: HashSet<IpAddr>,
|
||||
port: u16,
|
||||
pos: Position,
|
||||
active: bool,
|
||||
) -> ClientHandle {
|
||||
// get a new client_handle
|
||||
let handle = self.free_id();
|
||||
@@ -119,15 +134,12 @@ impl ClientManager {
|
||||
// store fix ip addresses
|
||||
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
|
||||
let client = Client {
|
||||
hostname,
|
||||
fix_ips,
|
||||
handle,
|
||||
addrs,
|
||||
ips,
|
||||
port,
|
||||
pos,
|
||||
};
|
||||
@@ -135,7 +147,7 @@ impl ClientManager {
|
||||
// client was never seen, nor pinged
|
||||
let client_state = ClientState {
|
||||
client,
|
||||
active: false,
|
||||
active,
|
||||
active_addr: None,
|
||||
alive: false,
|
||||
pressed_keys: HashSet::new(),
|
||||
@@ -158,7 +170,7 @@ impl ClientManager {
|
||||
.iter()
|
||||
.position(|c| {
|
||||
if let Some(c) = c {
|
||||
c.active && c.client.addrs.contains(&addr)
|
||||
c.active && c.client.ips.contains(&addr.ip())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ use std::{error::Error, fs};
|
||||
use toml;
|
||||
|
||||
use crate::client::Position;
|
||||
use crate::scancode;
|
||||
use crate::scancode::Linux::{KeyLeftAlt, KeyLeftCtrl, KeyLeftMeta, KeyLeftShift};
|
||||
|
||||
pub const DEFAULT_PORT: u16 = 4242;
|
||||
|
||||
@@ -15,17 +17,20 @@ pub const DEFAULT_PORT: u16 = 4242;
|
||||
pub struct ConfigToml {
|
||||
pub port: Option<u16>,
|
||||
pub frontend: Option<String>,
|
||||
pub left: Option<Client>,
|
||||
pub right: Option<Client>,
|
||||
pub top: Option<Client>,
|
||||
pub bottom: Option<Client>,
|
||||
pub release_bind: Option<Vec<scancode::Linux>>,
|
||||
pub left: Option<TomlClient>,
|
||||
pub right: Option<TomlClient>,
|
||||
pub top: Option<TomlClient>,
|
||||
pub bottom: Option<TomlClient>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
pub struct Client {
|
||||
pub struct TomlClient {
|
||||
pub hostname: Option<String>,
|
||||
pub host_name: Option<String>,
|
||||
pub ips: Option<Vec<IpAddr>>,
|
||||
pub port: Option<u16>,
|
||||
pub activate_on_startup: Option<bool>,
|
||||
}
|
||||
|
||||
impl ConfigToml {
|
||||
@@ -66,10 +71,22 @@ pub enum Frontend {
|
||||
pub struct Config {
|
||||
pub frontend: Frontend,
|
||||
pub port: u16,
|
||||
pub clients: Vec<(Client, Position)>,
|
||||
pub clients: Vec<(TomlClient, Position)>,
|
||||
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 {
|
||||
pub fn new() -> Result<Self> {
|
||||
let args = CliArgs::parse();
|
||||
@@ -93,7 +110,7 @@ impl Config {
|
||||
|
||||
let config_toml = match ConfigToml::new(config_path.as_str()) {
|
||||
Err(e) => {
|
||||
log::error!("{config_path}: {e}");
|
||||
log::warn!("{config_path}: {e}");
|
||||
log::warn!("Continuing without config file ...");
|
||||
None
|
||||
}
|
||||
@@ -128,7 +145,13 @@ impl Config {
|
||||
},
|
||||
};
|
||||
|
||||
let mut clients: Vec<(Client, Position)> = vec![];
|
||||
log::debug!("{config_toml:?}");
|
||||
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(c) = config_toml.right {
|
||||
@@ -152,21 +175,32 @@ impl Config {
|
||||
frontend,
|
||||
clients,
|
||||
port,
|
||||
release_bind,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_clients(&self) -> Vec<(HashSet<IpAddr>, Option<String>, u16, Position)> {
|
||||
pub fn get_clients(&self) -> Vec<ConfigClient> {
|
||||
self.clients
|
||||
.iter()
|
||||
.map(|(c, p)| {
|
||||
.map(|(c, pos)| {
|
||||
let port = c.port.unwrap_or(DEFAULT_PORT);
|
||||
let ips: HashSet<IpAddr> = if let Some(ips) = c.ips.as_ref() {
|
||||
HashSet::from_iter(ips.iter().cloned())
|
||||
} else {
|
||||
HashSet::new()
|
||||
};
|
||||
let host_name = c.host_name.clone();
|
||||
(ips, host_name, port, *p)
|
||||
let hostname = match &c.hostname {
|
||||
Some(h) => Some(h.clone()),
|
||||
None => c.host_name.clone(),
|
||||
};
|
||||
let active = c.activate_on_startup.unwrap_or(false);
|
||||
ConfigClient {
|
||||
ips,
|
||||
hostname,
|
||||
port,
|
||||
pos: *pos,
|
||||
active,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -103,8 +103,9 @@ pub enum FrontendEvent {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum FrontendNotify {
|
||||
NotifyClientCreate(ClientHandle, Option<String>, u16, Position),
|
||||
NotifyClientUpdate(ClientHandle, Option<String>, u16, Position),
|
||||
NotifyClientActivate(ClientHandle, bool),
|
||||
NotifyClientCreate(Client),
|
||||
NotifyClientUpdate(Client),
|
||||
NotifyClientDelete(ClientHandle),
|
||||
/// new port, reason of failure (if failed)
|
||||
NotifyPortChange(u16, Option<String>),
|
||||
@@ -224,7 +225,6 @@ impl FrontendListener {
|
||||
log::debug!("json: {json}, len: {}", payload.len());
|
||||
|
||||
let mut keep = vec![];
|
||||
|
||||
// TODO do simultaneously
|
||||
for tx in self.tx_streams.iter_mut() {
|
||||
// write len + payload
|
||||
|
||||
@@ -83,17 +83,26 @@ pub fn run() -> Result<()> {
|
||||
Err(e) => break log::error!("{e}"),
|
||||
};
|
||||
match notify {
|
||||
FrontendNotify::NotifyClientCreate(client, host, port, pos) => {
|
||||
log::info!(
|
||||
"new client ({client}): {}:{port} - {pos}",
|
||||
host.as_deref().unwrap_or("")
|
||||
);
|
||||
FrontendNotify::NotifyClientActivate(handle, active) => {
|
||||
if active {
|
||||
log::info!("client {handle} activated");
|
||||
} else {
|
||||
log::info!("client {handle} deactivated");
|
||||
}
|
||||
}
|
||||
FrontendNotify::NotifyClientUpdate(client, host, port, pos) => {
|
||||
log::info!(
|
||||
"client ({client}) updated: {}:{port} - {pos}",
|
||||
host.as_deref().unwrap_or("")
|
||||
);
|
||||
FrontendNotify::NotifyClientCreate(client) => {
|
||||
let handle = client.handle;
|
||||
let port = client.port;
|
||||
let pos = client.pos;
|
||||
let hostname = client.hostname.as_deref().unwrap_or("");
|
||||
log::info!("new client ({handle}): {hostname}:{port} - {pos}");
|
||||
}
|
||||
FrontendNotify::NotifyClientUpdate(client) => {
|
||||
let handle = client.handle;
|
||||
let port = client.port;
|
||||
let pos = client.pos;
|
||||
let hostname = client.hostname.as_deref().unwrap_or("");
|
||||
log::info!("client ({handle}) updated: {hostname}:{port} - {pos}");
|
||||
}
|
||||
FrontendNotify::NotifyClientDelete(client) => {
|
||||
log::info!("client ({client}) deleted.");
|
||||
@@ -109,7 +118,7 @@ pub fn run() -> Result<()> {
|
||||
client.hostname.as_deref().unwrap_or(""),
|
||||
if active { "yes" } else { "no" },
|
||||
client
|
||||
.addrs
|
||||
.ips
|
||||
.into_iter()
|
||||
.map(|a| a.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
|
||||
@@ -8,16 +8,12 @@ use std::{
|
||||
process, str,
|
||||
};
|
||||
|
||||
use crate::{config::DEFAULT_PORT, frontend::gtk::window::Window};
|
||||
use crate::frontend::gtk::window::Window;
|
||||
|
||||
use adw::Application;
|
||||
use gtk::{
|
||||
gdk::Display,
|
||||
gio::{SimpleAction, SimpleActionGroup},
|
||||
glib::clone,
|
||||
prelude::*,
|
||||
subclass::prelude::ObjectSubclassIsExt,
|
||||
CssProvider, IconTheme,
|
||||
gdk::Display, glib::clone, prelude::*, subclass::prelude::ObjectSubclassIsExt, CssProvider,
|
||||
IconTheme,
|
||||
};
|
||||
use gtk::{gio, glib, prelude::ApplicationExt};
|
||||
|
||||
@@ -38,7 +34,12 @@ pub fn run() -> glib::ExitCode {
|
||||
#[cfg(not(windows))]
|
||||
let ret = gtk_main();
|
||||
|
||||
log::debug!("frontend exited");
|
||||
if ret == glib::ExitCode::FAILURE {
|
||||
log::error!("frontend exited with failure");
|
||||
} else {
|
||||
log::info!("frontend exited successfully");
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
@@ -46,7 +47,7 @@ fn gtk_main() -> glib::ExitCode {
|
||||
gio::resources_register_include!("lan-mouse.gresource").expect("Failed to register resources.");
|
||||
|
||||
let app = Application::builder()
|
||||
.application_id("de.feschber.lan-mouse")
|
||||
.application_id("de.feschber.LanMouse")
|
||||
.build();
|
||||
|
||||
app.connect_startup(|_| load_icons());
|
||||
@@ -68,8 +69,8 @@ fn load_css() {
|
||||
}
|
||||
|
||||
fn load_icons() {
|
||||
let icon_theme =
|
||||
IconTheme::for_display(&Display::default().expect("Could not connect to a display."));
|
||||
let display = &Display::default().expect("Could not connect to a display.");
|
||||
let icon_theme = IconTheme::for_display(display);
|
||||
icon_theme.add_resource_path("/de/feschber/LanMouse/icons");
|
||||
}
|
||||
|
||||
@@ -130,15 +131,17 @@ fn build_ui(app: &Application) {
|
||||
loop {
|
||||
let notify = receiver.recv().await.unwrap();
|
||||
match notify {
|
||||
FrontendNotify::NotifyClientCreate(client, hostname, port, position) => {
|
||||
window.new_client(client, hostname, port, position, false);
|
||||
FrontendNotify::NotifyClientActivate(handle, active) => {
|
||||
window.activate_client(handle, active);
|
||||
}
|
||||
FrontendNotify::NotifyClientCreate(client) => {
|
||||
window.new_client(client, false);
|
||||
},
|
||||
FrontendNotify::NotifyClientUpdate(client, hostname, port, position) => {
|
||||
log::info!("client updated: {client}, {}:{port}, {position}", hostname.unwrap_or("".to_string()));
|
||||
FrontendNotify::NotifyClientUpdate(client) => {
|
||||
window.update_client(client);
|
||||
}
|
||||
FrontendNotify::NotifyError(e) => {
|
||||
// TODO
|
||||
log::error!("{e}");
|
||||
window.show_toast(e.as_str());
|
||||
},
|
||||
FrontendNotify::NotifyClientDelete(client) => {
|
||||
window.delete_client(client);
|
||||
@@ -146,19 +149,11 @@ fn build_ui(app: &Application) {
|
||||
FrontendNotify::Enumerate(clients) => {
|
||||
for (client, active) in clients {
|
||||
if window.client_idx(client.handle).is_some() {
|
||||
continue
|
||||
window.activate_client(client.handle, active);
|
||||
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) => {
|
||||
@@ -172,37 +167,5 @@ 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();
|
||||
}
|
||||
|
||||
@@ -3,26 +3,20 @@ mod imp;
|
||||
use adw::subclass::prelude::*;
|
||||
use gtk::glib::{self, Object};
|
||||
|
||||
use crate::client::ClientHandle;
|
||||
use crate::client::{Client, ClientHandle};
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct ClientObject(ObjectSubclass<imp::ClientObject>);
|
||||
}
|
||||
|
||||
impl ClientObject {
|
||||
pub fn new(
|
||||
handle: ClientHandle,
|
||||
hostname: Option<String>,
|
||||
port: u32,
|
||||
position: String,
|
||||
active: bool,
|
||||
) -> Self {
|
||||
pub fn new(client: Client, active: bool) -> Self {
|
||||
Object::builder()
|
||||
.property("handle", handle)
|
||||
.property("hostname", hostname)
|
||||
.property("port", port)
|
||||
.property("handle", client.handle)
|
||||
.property("hostname", client.hostname)
|
||||
.property("port", client.port as u32)
|
||||
.property("position", client.pos.to_string())
|
||||
.property("active", active)
|
||||
.property("position", position)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,12 @@ impl ClientRow {
|
||||
.sync_create()
|
||||
.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
|
||||
.bind_property("hostname", &self.imp().hostname.get(), "text")
|
||||
.transform_to(|_, v: Option<String>| {
|
||||
@@ -104,6 +110,7 @@ impl ClientRow {
|
||||
.build();
|
||||
|
||||
bindings.push(active_binding);
|
||||
bindings.push(switch_position_binding);
|
||||
bindings.push(hostname_binding);
|
||||
bindings.push(title_binding);
|
||||
bindings.push(port_binding);
|
||||
|
||||
@@ -4,7 +4,9 @@ use adw::subclass::prelude::*;
|
||||
use adw::{prelude::*, ActionRow, ComboRow};
|
||||
use glib::{subclass::InitializingObject, Binding};
|
||||
use gtk::glib::clone;
|
||||
use gtk::glib::subclass::Signal;
|
||||
use gtk::{glib, Button, CompositeTemplate, Switch};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
#[derive(CompositeTemplate, Default)]
|
||||
#[template(resource = "/de/feschber/LanMouse/client_row.ui")]
|
||||
@@ -28,6 +30,8 @@ pub struct ClientRow {
|
||||
impl ObjectSubclass for ClientRow {
|
||||
// `NAME` needs to match `class` attribute of template
|
||||
const NAME: &'static str = "ClientRow";
|
||||
const ABSTRACT: bool = false;
|
||||
|
||||
type Type = super::ClientRow;
|
||||
type ParentType = adw::ExpanderRow;
|
||||
|
||||
@@ -49,28 +53,33 @@ impl ObjectImpl for ClientRow {
|
||||
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]
|
||||
impl ClientRow {
|
||||
#[template_callback]
|
||||
fn handle_client_set_state(&self, state: bool, switch: &Switch) -> bool {
|
||||
let idx = self.obj().index() as u32;
|
||||
switch
|
||||
.activate_action("win.request-client-update", Some(&idx.to_variant()))
|
||||
.unwrap();
|
||||
switch.set_state(state);
|
||||
|
||||
fn handle_client_set_state(&self, state: bool, _switch: &Switch) -> bool {
|
||||
log::debug!("state change -> requesting update");
|
||||
self.obj().emit_by_name::<()>("request-update", &[&state]);
|
||||
true // dont run default handler
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn handle_client_delete(&self, button: &Button) {
|
||||
log::debug!("delete button pressed");
|
||||
let idx = self.obj().index() as u32;
|
||||
button
|
||||
.activate_action("win.request-client-delete", Some(&idx.to_variant()))
|
||||
.unwrap();
|
||||
fn handle_client_delete(&self, _button: &Button) {
|
||||
log::debug!("delete button pressed -> requesting delete");
|
||||
self.obj().emit_by_name::<()>("request-delete", &[]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,14 @@ use std::io::Write;
|
||||
use adw::prelude::*;
|
||||
use adw::subclass::prelude::*;
|
||||
use glib::{clone, Object};
|
||||
use gtk::{gio, glib, NoSelection};
|
||||
use gtk::{
|
||||
gio,
|
||||
glib::{self, closure_local},
|
||||
NoSelection,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
client::{ClientHandle, Position},
|
||||
client::{Client, ClientHandle, Position},
|
||||
config::DEFAULT_PORT,
|
||||
frontend::{gtk::client_object::ClientObject, FrontendEvent},
|
||||
};
|
||||
@@ -45,6 +49,18 @@ impl Window {
|
||||
clone!(@weak self as window => @default-panic, move |obj| {
|
||||
let client_object = obj.downcast_ref().expect("Expected object of type `ClientObject`.");
|
||||
let row = window.create_client_row(client_object);
|
||||
row.connect_closure("request-update", false, closure_local!(@strong window => move |row: ClientRow, active: bool| {
|
||||
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()
|
||||
})
|
||||
);
|
||||
@@ -62,7 +78,7 @@ impl Window {
|
||||
}
|
||||
|
||||
fn setup_icon(&self) {
|
||||
self.set_icon_name(Some("mouse-icon"));
|
||||
self.set_icon_name(Some("de.feschber.LanMouse"));
|
||||
}
|
||||
|
||||
fn create_client_row(&self, client_object: &ClientObject) -> ClientRow {
|
||||
@@ -71,15 +87,8 @@ impl Window {
|
||||
row
|
||||
}
|
||||
|
||||
pub fn new_client(
|
||||
&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);
|
||||
pub fn new_client(&self, client: Client, active: bool) {
|
||||
let client = ClientObject::new(client, active);
|
||||
self.clients().append(&client);
|
||||
self.set_placeholder_visible(false);
|
||||
}
|
||||
@@ -106,6 +115,42 @@ 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) {
|
||||
let event = FrontendEvent::AddClient(None, DEFAULT_PORT, Position::default());
|
||||
self.imp().set_port(DEFAULT_PORT);
|
||||
@@ -121,13 +166,10 @@ impl Window {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_client_update(&self, client: &ClientObject) {
|
||||
pub fn request_client_update(&self, client: &ClientObject, active: bool) {
|
||||
let data = client.get_data();
|
||||
let position = match data.position.as_str() {
|
||||
"left" => Position::Left,
|
||||
"right" => Position::Right,
|
||||
"top" => Position::Top,
|
||||
"bottom" => Position::Bottom,
|
||||
let position = match Position::try_from(data.position.as_str()) {
|
||||
Ok(pos) => pos,
|
||||
_ => {
|
||||
log::error!("invalid position: {}", data.position);
|
||||
return;
|
||||
@@ -135,10 +177,13 @@ impl Window {
|
||||
};
|
||||
let hostname = data.hostname;
|
||||
let port = data.port as u16;
|
||||
|
||||
let event = FrontendEvent::UpdateClient(client.handle(), hostname, port, position);
|
||||
log::debug!("requesting update: {event:?}");
|
||||
self.request(event);
|
||||
|
||||
let event = FrontendEvent::ActivateClient(client.handle(), !client.active());
|
||||
let event = FrontendEvent::ActivateClient(client.handle(), active);
|
||||
log::debug!("requesting activate: {event:?}");
|
||||
self.request(event);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,7 @@ use std::net::TcpStream;
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
use adw::subclass::prelude::*;
|
||||
use adw::{
|
||||
prelude::{EditableExt, WidgetExt},
|
||||
ActionRow, ToastOverlay,
|
||||
};
|
||||
use adw::{prelude::*, ActionRow, ToastOverlay};
|
||||
use glib::subclass::InitializingObject;
|
||||
use gtk::{gio, glib, Button, CompositeTemplate, Entry, ListBox};
|
||||
|
||||
@@ -42,6 +39,8 @@ pub struct Window {
|
||||
impl ObjectSubclass for Window {
|
||||
// `NAME` needs to match `class` attribute of template
|
||||
const NAME: &'static str = "LanMouseWindow";
|
||||
const ABSTRACT: bool = false;
|
||||
|
||||
type Type = super::Window;
|
||||
type ParentType = adw::ApplicationWindow;
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ pub fn run() -> Result<()> {
|
||||
// parse config file + cli args
|
||||
let config = Config::new()?;
|
||||
log::debug!("{config:?}");
|
||||
log::info!("release bind: {:?}", config.release_bind);
|
||||
|
||||
if config.daemon {
|
||||
// if daemon is specified we run the service
|
||||
@@ -38,7 +39,6 @@ pub fn run() -> Result<()> {
|
||||
// run a frontend
|
||||
let mut service = start_service()?;
|
||||
frontend::run_frontend(&config)?;
|
||||
log::info!("terminating service");
|
||||
#[cfg(unix)]
|
||||
{
|
||||
// on unix we give the service a chance to terminate gracefully
|
||||
|
||||
@@ -22,7 +22,7 @@ pub async fn create() -> Box<dyn EventProducer> {
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
match producer::libei::LibeiProducer::new() {
|
||||
match producer::libei::LibeiProducer::new().await {
|
||||
Ok(p) => {
|
||||
log::info!("using libei event producer");
|
||||
return Box::new(p);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/*
|
||||
* https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input
|
||||
*/
|
||||
@@ -165,7 +167,7 @@ pub enum Windows {
|
||||
* https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h
|
||||
*/
|
||||
#[repr(u32)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, Hash, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
pub enum Linux {
|
||||
KeyReserved = 0,
|
||||
@@ -210,7 +212,7 @@ pub enum Linux {
|
||||
KeySemicolon = 39,
|
||||
KeyApostrophe = 40,
|
||||
KeyGrave = 41,
|
||||
KeyLeftshift = 42,
|
||||
KeyLeftShift = 42,
|
||||
KeyBackslash = 43,
|
||||
KeyZ = 44,
|
||||
KeyX = 45,
|
||||
@@ -224,7 +226,7 @@ pub enum Linux {
|
||||
KeySlash = 53,
|
||||
KeyRightShift = 54,
|
||||
KeyKpAsterisk = 55,
|
||||
KeyLeftalt = 56,
|
||||
KeyLeftAlt = 56,
|
||||
KeySpace = 57,
|
||||
KeyCapsLock = 58,
|
||||
KeyF1 = 59,
|
||||
@@ -294,7 +296,7 @@ pub enum Linux {
|
||||
// KEY_HANGUEL = KeyHangeul,
|
||||
KeyHanja = 123,
|
||||
KeyYen = 124,
|
||||
KeyLeftmeta = 125,
|
||||
KeyLeftMeta = 125,
|
||||
KeyRightmeta = 126,
|
||||
KeyCompose = 127,
|
||||
KeyStop = 128, /* AC Stop */
|
||||
@@ -485,7 +487,7 @@ impl TryFrom<Linux> for Windows {
|
||||
Linux::KeySemicolon => Ok(Self::KeySemiColon),
|
||||
Linux::KeyApostrophe => Ok(Self::KeyApostrophe),
|
||||
Linux::KeyGrave => Ok(Self::KeyGrave),
|
||||
Linux::KeyLeftshift => Ok(Self::KeyLeftShift),
|
||||
Linux::KeyLeftShift => Ok(Self::KeyLeftShift),
|
||||
Linux::KeyBackslash => Ok(Self::KeyBackslash),
|
||||
Linux::KeyZ => Ok(Self::KeyZ),
|
||||
Linux::KeyX => Ok(Self::KeyX),
|
||||
@@ -499,7 +501,7 @@ impl TryFrom<Linux> for Windows {
|
||||
Linux::KeySlash => Ok(Self::KeySlash),
|
||||
Linux::KeyRightShift => Ok(Self::KeyRightShift),
|
||||
Linux::KeyKpAsterisk => Ok(Self::KeypadStar),
|
||||
Linux::KeyLeftalt => Ok(Self::KeyLeftAlt),
|
||||
Linux::KeyLeftAlt => Ok(Self::KeyLeftAlt),
|
||||
Linux::KeySpace => Ok(Self::KeySpace),
|
||||
Linux::KeyCapsLock => Ok(Self::KeyCapsLock),
|
||||
Linux::KeyF1 => Ok(Self::KeyF1),
|
||||
@@ -567,7 +569,7 @@ impl TryFrom<Linux> for Windows {
|
||||
Linux::KeyHangeul => Ok(Self::KeyInternational1), // TODO unsure
|
||||
Linux::KeyHanja => Ok(Self::KeyInternational2), // TODO unsure
|
||||
Linux::KeyYen => Ok(Self::KeyInternational3), // TODO unsure
|
||||
Linux::KeyLeftmeta => Ok(Self::KeyLeftGUI),
|
||||
Linux::KeyLeftMeta => Ok(Self::KeyLeftGUI),
|
||||
Linux::KeyRightmeta => Ok(Self::KeyRightGUI),
|
||||
Linux::KeyCompose => Ok(Self::KeyApplication),
|
||||
Linux::KeyStop => Ok(Self::ACStop),
|
||||
|
||||
975
src/server.rs
975
src/server.rs
File diff suppressed because it is too large
Load Diff
240
src/server/consumer_task.rs
Normal file
240
src/server/consumer_task.rs
Normal file
@@ -0,0 +1,240 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use tokio::{
|
||||
sync::mpsc::{Receiver, Sender},
|
||||
task::JoinHandle,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
client::{ClientEvent, ClientHandle},
|
||||
consumer::EventConsumer,
|
||||
event::{Event, KeyboardEvent},
|
||||
scancode,
|
||||
server::State,
|
||||
};
|
||||
|
||||
use super::{ProducerEvent, Server};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ConsumerEvent {
|
||||
/// consumer is notified of a change in client states
|
||||
ClientEvent(ClientEvent),
|
||||
/// consumer must release keys for client
|
||||
ReleaseKeys(ClientHandle),
|
||||
/// termination signal
|
||||
Terminate,
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
mut consumer: Box<dyn EventConsumer>,
|
||||
server: Server,
|
||||
mut udp_rx: Receiver<Result<(Event, SocketAddr)>>,
|
||||
sender_tx: Sender<(Event, SocketAddr)>,
|
||||
producer_tx: Sender<ProducerEvent>,
|
||||
timer_tx: Sender<()>,
|
||||
) -> (JoinHandle<Result<()>>, Sender<ConsumerEvent>) {
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
|
||||
let consumer_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, &producer_tx, &mut consumer, &sender_tx, &mut last_ignored, udp_event, &timer_tx).await;
|
||||
}
|
||||
consumer_event = rx.recv() => {
|
||||
match consumer_event {
|
||||
Some(e) => match e {
|
||||
ConsumerEvent::ClientEvent(e) => consumer.notify(e).await,
|
||||
ConsumerEvent::ReleaseKeys(c) => release_keys(&server, &mut consumer, c).await,
|
||||
ConsumerEvent::Terminate => break,
|
||||
},
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
res = consumer.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 consumer, client).await;
|
||||
}
|
||||
|
||||
// destroy consumer
|
||||
consumer.destroy().await;
|
||||
anyhow::Ok(())
|
||||
});
|
||||
(consumer_task, tx)
|
||||
}
|
||||
|
||||
async fn handle_udp_rx(
|
||||
server: &Server,
|
||||
producer_notify_tx: &Sender<ProducerEvent>,
|
||||
consumer: &mut Box<dyn EventConsumer>,
|
||||
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, consumer, 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 _ = producer_notify_tx.send(ProducerEvent::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
|
||||
consumer.consume(event, handle).await;
|
||||
log::trace!("{event:?} => consumer");
|
||||
}
|
||||
}
|
||||
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 _ = producer_notify_tx.send(ProducerEvent::Release).await;
|
||||
log::trace!("STATE ===> Receiving");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn release_keys(
|
||||
server: &Server,
|
||||
consumer: &mut Box<dyn EventConsumer>,
|
||||
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,
|
||||
});
|
||||
consumer.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,
|
||||
};
|
||||
consumer
|
||||
.consume(Event::Keyboard(modifiers_event), client)
|
||||
.await;
|
||||
}
|
||||
335
src/server/frontend_task.rs
Normal file
335
src/server/frontend_task.rs
Normal file
@@ -0,0 +1,335 @@
|
||||
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::{
|
||||
consumer_task::ConsumerEvent, producer_task::ProducerEvent, resolver_task::DnsRequest, Server,
|
||||
};
|
||||
|
||||
pub(crate) fn new(
|
||||
mut frontend: FrontendListener,
|
||||
mut notify_rx: Receiver<FrontendNotify>,
|
||||
server: Server,
|
||||
producer_notify: Sender<ProducerEvent>,
|
||||
consumer_notify: Sender<ConsumerEvent>,
|
||||
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, &producer_notify, &consumer_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,
|
||||
producer_tx: &Sender<ProducerEvent>,
|
||||
consumer_tx: &Sender<ConsumerEvent>,
|
||||
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, producer_tx, consumer_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, producer_tx, consumer_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,
|
||||
producer_tx,
|
||||
consumer_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,
|
||||
producer_notify_tx: &Sender<ProducerEvent>,
|
||||
consumer_notify_tx: &Sender<ConsumerEvent>,
|
||||
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 _ = producer_notify_tx
|
||||
.send(ProducerEvent::ClientEvent(ClientEvent::Create(client, pos)))
|
||||
.await;
|
||||
let _ = consumer_notify_tx
|
||||
.send(ConsumerEvent::ClientEvent(ClientEvent::Create(client, pos)))
|
||||
.await;
|
||||
} else {
|
||||
let _ = producer_notify_tx
|
||||
.send(ProducerEvent::ClientEvent(ClientEvent::Destroy(client)))
|
||||
.await;
|
||||
let _ = consumer_notify_tx
|
||||
.send(ConsumerEvent::ClientEvent(ClientEvent::Destroy(client)))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn remove_client(
|
||||
server: &Server,
|
||||
producer_notify_tx: &Sender<ProducerEvent>,
|
||||
consumer_notify_tx: &Sender<ConsumerEvent>,
|
||||
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 _ = producer_notify_tx
|
||||
.send(ProducerEvent::ClientEvent(ClientEvent::Destroy(client)))
|
||||
.await;
|
||||
let _ = consumer_notify_tx
|
||||
.send(ConsumerEvent::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,
|
||||
producer_notify_tx: &Sender<ProducerEvent>,
|
||||
consumer_notify_tx: &Sender<ConsumerEvent>,
|
||||
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,
|
||||
)
|
||||
};
|
||||
|
||||
// update state in event consumer & producer
|
||||
if changed && active {
|
||||
// resolve dns
|
||||
if let Some(hostname) = hostname {
|
||||
let _ = resolve_tx.send(DnsRequest { hostname, handle }).await;
|
||||
}
|
||||
|
||||
// update state
|
||||
let _ = producer_notify_tx
|
||||
.send(ProducerEvent::ClientEvent(ClientEvent::Destroy(handle)))
|
||||
.await;
|
||||
let _ = consumer_notify_tx
|
||||
.send(ConsumerEvent::ClientEvent(ClientEvent::Destroy(handle)))
|
||||
.await;
|
||||
let _ = producer_notify_tx
|
||||
.send(ProducerEvent::ClientEvent(ClientEvent::Create(handle, pos)))
|
||||
.await;
|
||||
let _ = consumer_notify_tx
|
||||
.send(ConsumerEvent::ClientEvent(ClientEvent::Create(handle, pos)))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
90
src/server/network_task.rs
Normal file
90
src/server/network_task.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
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 event
|
||||
// producer.
|
||||
Ok(sock.try_send_to(&data, addr)?)
|
||||
}
|
||||
131
src/server/ping_task.rs
Normal file
131
src/server/ping_task.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
use std::{net::SocketAddr, time::Duration};
|
||||
|
||||
use tokio::{
|
||||
sync::mpsc::{Receiver, Sender},
|
||||
task::JoinHandle,
|
||||
};
|
||||
|
||||
use crate::{client::ClientHandle, event::Event};
|
||||
|
||||
use super::{consumer_task::ConsumerEvent, producer_task::ProducerEvent, Server, State};
|
||||
|
||||
const MAX_RESPONSE_TIME: Duration = Duration::from_millis(500);
|
||||
|
||||
pub fn new(
|
||||
server: Server,
|
||||
sender_ch: Sender<(Event, SocketAddr)>,
|
||||
consumer_notify: Sender<ConsumerEvent>,
|
||||
producer_notify: Sender<ProducerEvent>,
|
||||
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 _ = consumer_notify.send(ConsumerEvent::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 _ = producer_notify.send(ProducerEvent::Release).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
ping_task
|
||||
}
|
||||
152
src/server/producer_task.rs
Normal file
152
src/server/producer_task.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::StreamExt;
|
||||
use std::{collections::HashSet, net::SocketAddr};
|
||||
|
||||
use tokio::{sync::mpsc::Sender, task::JoinHandle};
|
||||
|
||||
use crate::{
|
||||
client::{ClientEvent, ClientHandle},
|
||||
event::{Event, KeyboardEvent},
|
||||
producer::EventProducer,
|
||||
scancode,
|
||||
server::State,
|
||||
};
|
||||
|
||||
use super::Server;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum ProducerEvent {
|
||||
/// producer must release the mouse
|
||||
Release,
|
||||
/// producer is notified of a change in client states
|
||||
ClientEvent(ClientEvent),
|
||||
/// termination signal
|
||||
Terminate,
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
mut producer: Box<dyn EventProducer>,
|
||||
server: Server,
|
||||
sender_tx: Sender<(Event, SocketAddr)>,
|
||||
timer_tx: Sender<()>,
|
||||
release_bind: Vec<scancode::Linux>,
|
||||
) -> (JoinHandle<Result<()>>, Sender<ProducerEvent>) {
|
||||
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 = producer.next() => {
|
||||
match event {
|
||||
Some(Ok(event)) => handle_producer_event(&server, &mut producer, &sender_tx, &timer_tx, event, &mut pressed_keys, &release_bind).await?,
|
||||
Some(Err(e)) => return Err(anyhow!("event producer: {e:?}")),
|
||||
None => return Err(anyhow!("event producer closed")),
|
||||
}
|
||||
}
|
||||
e = rx.recv() => {
|
||||
log::debug!("producer notify rx: {e:?}");
|
||||
match e {
|
||||
Some(e) => match e {
|
||||
ProducerEvent::Release => {
|
||||
producer.release()?;
|
||||
server.state.replace(State::Receiving);
|
||||
|
||||
}
|
||||
ProducerEvent::ClientEvent(e) => producer.notify(e)?,
|
||||
ProducerEvent::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_producer_event(
|
||||
server: &Server,
|
||||
producer: &mut Box<dyn EventProducer>,
|
||||
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");
|
||||
producer.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!");
|
||||
producer.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(())
|
||||
}
|
||||
40
src/server/resolver_task.rs
Normal file
40
src/server/resolver_task.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user