mirror of
https://github.com/feschber/lan-mouse.git
synced 2026-03-08 12:30:00 +03:00
Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a6d9a9fa9 | ||
|
|
0fffd5bb15 | ||
|
|
b0df901fcc | ||
|
|
9c0cc98dc0 | ||
|
|
5c152b0cbe | ||
|
|
e155819542 | ||
|
|
4b6faea93a | ||
|
|
80d8a496bb | ||
|
|
53e1af0780 | ||
|
|
d3fed1b769 | ||
|
|
cdd3a3b818 | ||
|
|
1cefa38543 | ||
|
|
fed8e02d9f | ||
|
|
65a12735e2 | ||
|
|
3484cab28c | ||
|
|
256d2107bd | ||
|
|
3ac738fb52 | ||
|
|
2ac9277fb0 | ||
|
|
d9fa86ef00 | ||
|
|
015facec39 | ||
|
|
66de3e3cbc | ||
|
|
4600db7af8 | ||
|
|
9f23e1a75d | ||
|
|
a24b231e3c | ||
|
|
8de6c9bb87 | ||
|
|
6766886377 | ||
|
|
a6ab109fae | ||
|
|
06c4e92d43 | ||
|
|
f5a0ff4f3a | ||
|
|
eca367cdb4 | ||
|
|
735434438f | ||
|
|
02d1b33e45 | ||
|
|
19143b90a5 | ||
|
|
ad2aeae275 | ||
|
|
0bdb1bc753 | ||
|
|
9140f60c69 | ||
|
|
0fbf9f4dc2 | ||
|
|
f13e25af82 | ||
|
|
48f7ad3592 | ||
|
|
5fc02d471b | ||
|
|
0c275bc2de | ||
|
|
010db79918 | ||
|
|
ebf5a64f20 | ||
|
|
5c8ea25563 | ||
|
|
b472b56b10 | ||
|
|
18edb0dbad | ||
|
|
622b820c7f | ||
|
|
09bf535eec | ||
|
|
39acce8e6a | ||
|
|
e3f9947284 | ||
|
|
5a7e0cf89c | ||
|
|
56e5f7a30d | ||
|
|
9b242f6138 | ||
|
|
b01f7c2793 | ||
|
|
61b23c910b | ||
|
|
74eebc07d8 | ||
|
|
e6677c3061 | ||
|
|
e88241e816 | ||
|
|
60a73b3cb0 | ||
|
|
cc28827721 | ||
|
|
dd1fb29f51 | ||
|
|
be0fe9f2d9 | ||
|
|
4cdc5ea49c | ||
|
|
96ab7d304b | ||
|
|
ab2514e508 | ||
|
|
d4d6f05802 | ||
|
|
79fa42b74e | ||
|
|
851b6d60eb | ||
|
|
06725f4b14 | ||
|
|
603646c799 | ||
|
|
b2179e88de | ||
|
|
bae52eb9e7 | ||
|
|
0fbd09b07f | ||
|
|
96dd9c05a1 | ||
|
|
15c02ac505 | ||
|
|
08893a39be | ||
|
|
48b701b726 | ||
|
|
891e21d3e9 | ||
|
|
6a5de3f025 | ||
|
|
1eb12baf15 | ||
|
|
65048abcfc | ||
|
|
d042c0aa4a | ||
|
|
c50b746816 | ||
|
|
3b09abb532 |
62
.github/workflows/pre-release.yml
vendored
62
.github/workflows/pre-release.yml
vendored
@@ -18,6 +18,7 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libx11-dev libxtst-dev
|
||||
sudo apt-get install libadwaita-1-dev libgtk-4-dev
|
||||
- name: Release Build
|
||||
run: cargo build --release
|
||||
- name: Upload build artifact
|
||||
@@ -29,18 +30,72 @@ jobs:
|
||||
windows-release-build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
# needed for cache restore
|
||||
- name: create gtk dir
|
||||
run: mkdir C:\gtk-build\gtk\x64\release
|
||||
- uses: actions/cache@v3
|
||||
id: cache
|
||||
with:
|
||||
path: c:/gtk-build/gtk/x64/release/**
|
||||
key: gtk-windows-build
|
||||
restore-keys: gtk-windows-build
|
||||
- name: Update path
|
||||
run: |
|
||||
echo "PKG_CONFIG=C:\gtk-build\gtk\x64\release\bin\pkgconf.exe" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
echo "C:\pkg-config-lite-0.28-1\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||
echo "C:\gtk-build\gtk\x64\release\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||
echo $env:GITHUB_PATH
|
||||
echo $env:PATH
|
||||
- name: Install dependencies
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
# choco install msys2
|
||||
# choco install visualstudio2022-workload-vctools
|
||||
# choco install pkgconfiglite
|
||||
pipx install gvsbuild
|
||||
# see https://github.com/wingtk/gvsbuild/pull/1004
|
||||
Move-Item "C:\Program Files\Git\usr\bin" "C:\Program Files\Git\usr\notbin"
|
||||
Move-Item "C:\Program Files\Git\bin" "C:\Program Files\Git\notbin"
|
||||
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
|
||||
- name: Release Build
|
||||
run: cargo build --release
|
||||
- name: Create Archive
|
||||
run: |
|
||||
mkdir "lan-mouse-windows"
|
||||
Get-Childitem -Path "C:\\gtk-build\\gtk\\x64\\release\\bin\\*.dll" -File -Recurse | Copy-Item -Destination "lan-mouse-windows"
|
||||
Copy-Item -Path "target\release\lan-mouse.exe" -Destination "lan-mouse-windows"
|
||||
Compress-Archive -Path "lan-mouse-windows\*" -DestinationPath lan-mouse-windows.zip
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: lan-mouse-windows
|
||||
path: target/release/lan-mouse.exe
|
||||
path: lan-mouse-windows.zip
|
||||
|
||||
macos-release-build:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: install dependencies
|
||||
run: brew install gtk4 libadwaita
|
||||
- name: Release Build
|
||||
run: |
|
||||
cargo build --release
|
||||
cp target/release/lan-mouse lan-mouse-macos-intel
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: lan-mouse-macos
|
||||
path: lan-mouse-macos-intel
|
||||
|
||||
pre-release:
|
||||
name: "Pre Release"
|
||||
needs: [windows-release-build, linux-release-build]
|
||||
needs: [windows-release-build, linux-release-build, macos-release-build]
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: Download build artifacts
|
||||
@@ -54,4 +109,5 @@ jobs:
|
||||
title: "Development Build"
|
||||
files: |
|
||||
lan-mouse-linux/lan-mouse
|
||||
lan-mouse-windows/lan-mouse.exe
|
||||
lan-mouse-macos/lan-mouse-macos-intel
|
||||
lan-mouse-windows/lan-mouse-windows.zip
|
||||
|
||||
67
.github/workflows/rust.yml
vendored
67
.github/workflows/rust.yml
vendored
@@ -20,10 +20,15 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libx11-dev libxtst-dev
|
||||
sudo apt-get install libadwaita-1-dev libgtk-4-dev
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
- name: Check Formatting
|
||||
run: cargo fmt --check
|
||||
- name: Clippy
|
||||
run: cargo clippy --all-features --all-targets -- --deny warnings
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@@ -36,12 +41,72 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
# needed for cache restore
|
||||
- name: create gtk dir
|
||||
run: mkdir C:\gtk-build\gtk\x64\release
|
||||
- uses: actions/cache@v3
|
||||
id: cache
|
||||
with:
|
||||
path: c:/gtk-build/gtk/x64/release/**
|
||||
key: gtk-windows-build
|
||||
restore-keys: gtk-windows-build
|
||||
- name: Update path
|
||||
run: |
|
||||
echo "PKG_CONFIG=C:\gtk-build\gtk\x64\release\bin\pkgconf.exe" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
echo "C:\pkg-config-lite-0.28-1\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||
echo "C:\gtk-build\gtk\x64\release\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||
echo $env:GITHUB_PATH
|
||||
echo $env:PATH
|
||||
- name: Install dependencies
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
# choco install msys2
|
||||
# choco install visualstudio2022-workload-vctools
|
||||
# choco install pkgconfiglite
|
||||
pipx install gvsbuild
|
||||
# see https://github.com/wingtk/gvsbuild/pull/1004
|
||||
Move-Item "C:\Program Files\Git\usr\bin" "C:\Program Files\Git\usr\notbin"
|
||||
Move-Item "C:\Program Files\Git\bin" "C:\Program Files\Git\notbin"
|
||||
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"
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
- name: Check Formatting
|
||||
run: cargo fmt --check
|
||||
- name: Clippy
|
||||
run: cargo clippy --all-features --all-targets -- --deny warnings
|
||||
- name: 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
|
||||
with:
|
||||
name: lan-mouse-windows
|
||||
path: target/debug/lan-mouse.exe
|
||||
path: |
|
||||
target/debug/lan-mouse.exe
|
||||
target/debug/*.dll
|
||||
|
||||
build-macos:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: install dependencies
|
||||
run: brew install gtk4 libadwaita
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
- name: Check Formatting
|
||||
run: cargo fmt --check
|
||||
- name: Clippy
|
||||
run: cargo clippy --all-features --all-targets -- --deny warnings
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: lan-mouse-macos
|
||||
path: target/debug/lan-mouse
|
||||
|
||||
62
.github/workflows/tagged-release.yml
vendored
62
.github/workflows/tagged-release.yml
vendored
@@ -14,6 +14,7 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libx11-dev libxtst-dev
|
||||
sudo apt-get install libadwaita-1-dev libgtk-4-dev
|
||||
- name: Release Build
|
||||
run: cargo build --release
|
||||
- name: Upload build artifact
|
||||
@@ -25,18 +26,72 @@ jobs:
|
||||
windows-release-build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
# needed for cache restore
|
||||
- name: create gtk dir
|
||||
run: mkdir C:\gtk-build\gtk\x64\release
|
||||
- uses: actions/cache@v3
|
||||
id: cache
|
||||
with:
|
||||
path: c:/gtk-build/gtk/x64/release/**
|
||||
key: gtk-windows-build
|
||||
restore-keys: gtk-windows-build
|
||||
- name: Update path
|
||||
run: |
|
||||
echo "PKG_CONFIG=C:\gtk-build\gtk\x64\release\bin\pkgconf.exe" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
echo "C:\pkg-config-lite-0.28-1\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||
echo "C:\gtk-build\gtk\x64\release\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||
echo $env:GITHUB_PATH
|
||||
echo $env:PATH
|
||||
- name: Install dependencies
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
# choco install msys2
|
||||
# choco install visualstudio2022-workload-vctools
|
||||
# choco install pkgconfiglite
|
||||
pipx install gvsbuild
|
||||
# see https://github.com/wingtk/gvsbuild/pull/1004
|
||||
Move-Item "C:\Program Files\Git\usr\bin" "C:\Program Files\Git\usr\notbin"
|
||||
Move-Item "C:\Program Files\Git\bin" "C:\Program Files\Git\notbin"
|
||||
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
|
||||
- name: Release Build
|
||||
run: cargo build --release
|
||||
- name: Create Archive
|
||||
run: |
|
||||
mkdir "lan-mouse-windows"
|
||||
Get-Childitem -Path "C:\\gtk-build\\gtk\\x64\\release\\bin\\*.dll" -File -Recurse | Copy-Item -Destination "lan-mouse-windows"
|
||||
Copy-Item -Path "target\release\lan-mouse.exe" -Destination "lan-mouse-windows"
|
||||
Compress-Archive -Path "lan-mouse-windows\*" -DestinationPath lan-mouse-windows.zip
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: lan-mouse-windows
|
||||
path: target/release/lan-mouse.exe
|
||||
path: lan-mouse-windows.zip
|
||||
|
||||
macos-release-build:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: install dependencies
|
||||
run: brew install gtk4 libadwaita
|
||||
- name: Release Build
|
||||
run: |
|
||||
cargo build --release
|
||||
cp target/release/lan-mouse lan-mouse-macos-intel
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: lan-mouse-macos
|
||||
path: lan-mouse-macos-intel
|
||||
|
||||
tagged-release:
|
||||
name: "Tagged Release"
|
||||
needs: [windows-release-build, linux-release-build]
|
||||
needs: [windows-release-build, linux-release-build, macos-release-build]
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: Download build artifacts
|
||||
@@ -48,4 +103,5 @@ jobs:
|
||||
prerelease: false
|
||||
files: |
|
||||
lan-mouse-linux/lan-mouse
|
||||
lan-mouse-windows/lan-mouse.exe
|
||||
lan-mouse-macos/lan-mouse-macos-intel
|
||||
lan-mouse-windows/lan-mouse-windows.zip
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1 +1,4 @@
|
||||
/target
|
||||
.gdbinit
|
||||
.idea/
|
||||
.vs/
|
||||
1486
Cargo.lock
generated
1486
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
51
Cargo.toml
51
Cargo.toml
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "lan-mouse"
|
||||
description = "Software KVM Switch / mouse & keyboard sharing software for Local Area Networks"
|
||||
version = "0.2.1"
|
||||
version = "0.5.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
repository = "https://github.com/ferdinandschober/lan-mouse"
|
||||
@@ -13,36 +13,49 @@ strip = true
|
||||
lto = "fat"
|
||||
|
||||
[dependencies]
|
||||
tempfile = "3.6"
|
||||
trust-dns-resolver = "0.22"
|
||||
tempfile = "3.8"
|
||||
trust-dns-resolver = "0.23"
|
||||
memmap = "0.7"
|
||||
toml = "0.7"
|
||||
toml = "0.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
anyhow = "1.0.71"
|
||||
log = "0.4.20"
|
||||
env_logger = "0.10.0"
|
||||
mio = { version = "0.8", features = ["os-ext"] }
|
||||
libc = "0.2.148"
|
||||
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 }
|
||||
async-channel = { version = "2.1.1", optional = true }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
wayland-client = { version="0.30.2", optional = true }
|
||||
wayland-protocols = { version="0.30.0", features=["client", "staging", "unstable"], optional = true }
|
||||
wayland-protocols-wlr = { version="0.1.0", features=["client"], optional = true }
|
||||
wayland-protocols-misc = { version="0.1.0", features=["client"], optional = true }
|
||||
wayland-protocols-plasma = { version="0.1.0", features=["client"], optional = true }
|
||||
mio-signals = "0.2.0"
|
||||
libc = "0.2.148"
|
||||
|
||||
[target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
|
||||
wayland-client = { version="0.31.1", optional = true }
|
||||
wayland-protocols = { version="0.31.0", features=["client", "staging", "unstable"], optional = true }
|
||||
wayland-protocols-wlr = { version="0.2.0", features=["client"], optional = true }
|
||||
wayland-protocols-misc = { version="0.2.0", features=["client"], optional = true }
|
||||
x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
|
||||
gtk = { package = "gtk4", version = "0.7.2", features = ["v4_8"], optional = true }
|
||||
adw = { package = "libadwaita", version = "0.5.2", features = ["v1_3"], 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 }
|
||||
|
||||
[target.'cfg(target_os="macos")'.dependencies]
|
||||
core-graphics = { version = "0.23", features = ["highsierra"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winapi = { version = "0.3.9", features = ["winuser"] }
|
||||
|
||||
[build-dependencies]
|
||||
glib-build-tools = "0.18.0"
|
||||
|
||||
[features]
|
||||
default = ["wayland", "x11", "xdg_desktop_portal", "libei"]
|
||||
wayland = ["dep:wayland-client", "dep:wayland-protocols", "dep:wayland-protocols-wlr", "dep:wayland-protocols-misc", "dep:wayland-protocols-plasma"]
|
||||
default = ["wayland", "x11", "xdg_desktop_portal", "libei", "gtk"]
|
||||
wayland = ["dep:wayland-client", "dep:wayland-protocols", "dep:wayland-protocols-wlr", "dep:wayland-protocols-misc" ]
|
||||
x11 = ["dep:x11"]
|
||||
xdg_desktop_portal = []
|
||||
libei = []
|
||||
gtk = ["dep:gtk", "dep:adw"]
|
||||
xdg_desktop_portal = ["dep:ashpd"]
|
||||
libei = ["dep:reis", "dep:ashpd"]
|
||||
gtk = ["dep:gtk", "dep:adw", "dep:async-channel"]
|
||||
|
||||
329
README.md
329
README.md
@@ -1,164 +1,224 @@
|
||||
# Lan Mouse Share
|
||||
# Lan Mouse
|
||||
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).
|
||||
|
||||
- _Now with a gtk frontend_
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/feschber/lan-mouse/assets/40996949/016a06a9-76db-4951-9dcc-127d012c59df">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://github.com/feschber/lan-mouse/assets/40996949/d6318340-f811-4e16-9d6e-d1b79883c709">
|
||||
<img alt="Screenshot of Lan-Mouse" srcset="https://github.com/feschber/lan-mouse/assets/40996949/016a06a9-76db-4951-9dcc-127d012c59df">
|
||||
</picture>
|
||||
|
||||
|
||||
Goal of this project is to be an open-source replacement for proprietary tools like [Synergy](https://symless.com/synergy), [Share Mouse](https://www.sharemouse.com/de/).
|
||||
|
||||
Focus lies on performance and a clean, manageable implementation that can easily be expanded to support additional backends like e.g. Android, iOS, ... .
|
||||
|
||||
Of course ***blazingly fast™*** and stable, because it's written in rust.
|
||||
***blazingly fast™*** because it's written in rust.
|
||||
|
||||
For an alternative (with slightly different goals) you may check out [Input Leap](https://github.com/input-leap).
|
||||
|
||||
## Configuration
|
||||
Configuration is done through the file `config.toml`,
|
||||
which must be located in the current working directory when
|
||||
executing lan-mouse.
|
||||
## OS Support
|
||||
|
||||
### Example config
|
||||
A minimal config file could look like this:
|
||||
The following table shows support for input emulation (to emulate events received from other clients) and
|
||||
input capture (to send events *to* other clients) on different operating systems:
|
||||
|
||||
```toml
|
||||
[left]
|
||||
host_name = "my-laptop"
|
||||
```
|
||||
| Backend | input emulation | input capture |
|
||||
|---------------------------|--------------------------|--------------------------------------|
|
||||
| Wayland (wlroots) | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| Wayland (KDE) | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| Wayland (Gnome) | :heavy_check_mark: | WIP |
|
||||
| X11 | :heavy_check_mark: | WIP |
|
||||
| Windows | :heavy_check_mark: | WIP |
|
||||
| MacOS | ( :heavy_check_mark: ) | WIP |
|
||||
|
||||
Where `left` can be either `left`, `right`, `top` or `bottom`.
|
||||
|
||||
|
||||
### Additional options
|
||||
Additionally
|
||||
- a preferred backend
|
||||
- a port override for the default port (4242)
|
||||
|
||||
can be specified.
|
||||
|
||||
Supported backends currently include "wlroots", "x11" and "windows".
|
||||
|
||||
These two options can also be specified via the commandline
|
||||
options `--backend` and `--port` respectively.
|
||||
Keycode translation is not yet implemented so on MacOS only mouse emulation works as of right now.
|
||||
|
||||
## Build and Run
|
||||
Build only
|
||||
|
||||
### Install 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
|
||||
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
|
||||
```
|
||||
|
||||
Run
|
||||
Run directly:
|
||||
```sh
|
||||
cargo run --release
|
||||
```
|
||||
|
||||
### Conditional Compilation
|
||||
|
||||
Currently only x11, wayland and windows are supported backends,
|
||||
Currently only x11, wayland, windows and MacOS are supported backends.
|
||||
Depending on the toolchain used, support for other platforms is omitted
|
||||
automatically (it does not make sense to build a Windows `.exe` with
|
||||
support for x11 and wayland backends).
|
||||
|
||||
However one might still want to omit support for e.g. wayland or x11 on
|
||||
However one might still want to omit support for e.g. wayland, x11 or libei on
|
||||
a Linux system.
|
||||
|
||||
This is possible through
|
||||
[cargo features](https://doc.rust-lang.org/cargo/reference/features.html)
|
||||
[cargo features](https://doc.rust-lang.org/cargo/reference/features.html).
|
||||
|
||||
E.g. if only wayland support is needed, the following command produces
|
||||
an executable with just support for wayland:
|
||||
```sh
|
||||
cargo build --no-default-features --features wayland
|
||||
```
|
||||
For a detailed list of available features, checkout the [Cargo.toml](./Cargo.toml)
|
||||
|
||||
## OS Support
|
||||
## Usage
|
||||
### Gtk Frontend
|
||||
By default the gtk frontend will open when running `lan-mouse`.
|
||||
|
||||
The following table shows support for Event receiving and event Emitting
|
||||
on different operating systems:
|
||||
To add a new connection, simply click the `Add` button on *both* devices,
|
||||
enter the corresponding hostname and activate it.
|
||||
|
||||
| Backend | Event Receiving | Event Emitting |
|
||||
|---------------------------|--------------------------|--------------------------------------|
|
||||
| Wayland (wlroots) | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| Wayland (KDE) | WIP | :heavy_check_mark: |
|
||||
| Wayland (Gnome) | TODO (libei support) | TODO (wlr-layer-shell not supported) |
|
||||
| X11 | WIP | TODO |
|
||||
| Windows | needs improvements | TODO |
|
||||
| MacOS | TODO (I dont own a Mac) | TODO (I dont own a Mac) |
|
||||
If the mouse can not be moved onto a device, make sure you have port `4242` (or the one selected)
|
||||
opened up in your firewall.
|
||||
|
||||
## Wayland compositor support
|
||||
### Input Emulation (for receiving events)
|
||||
On wayland input-emulation is in an early/unstable state as of writing this.
|
||||
### Command Line Interface
|
||||
The cli interface can be enabled using `--frontend cli` as commandline arguments.
|
||||
Type `help` to list the available commands.
|
||||
|
||||
Different compositors have different ways of enabling input emulation:
|
||||
E.g.:
|
||||
```sh
|
||||
$ cargo run --release -- --frontend cli
|
||||
(...)
|
||||
> connect <host> left|right|top|bottom
|
||||
(...)
|
||||
> list
|
||||
(...)
|
||||
> activate 0
|
||||
```
|
||||
|
||||
Most wlroots-based compositors like Hyprland and Sway support the following
|
||||
unstable wayland protocols for keyboard and mouse emulation:
|
||||
- [virtual-keyboard-unstable-v1](https://wayland.app/protocols/virtual-keyboard-unstable-v1)
|
||||
- [wlr-virtual-pointer-unstable-v1](https://wayland.app/protocols/wlr-virtual-pointer-unstable-v1) are used to emulate input on wlroots compositors
|
||||
### Daemon
|
||||
Lan Mouse can be launched in daemon mode to keep it running in the background.
|
||||
To do so, add `--daemon` to the commandline args:
|
||||
|
||||
KDE also has a protocol for input emulation ([kde-fake-input](https://wayland.app/protocols/kde-fake-input)), it is however not exposed to
|
||||
third party apps, so the recommended way of enabling input emulation in KDE is the
|
||||
[freedesktop remote-desktop-portal](https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.RemoteDesktop).
|
||||
```sh
|
||||
$ cargo run --release -- --daemon
|
||||
```
|
||||
|
||||
Gnome uses [libei](https://gitlab.freedesktop.org/libinput/libei) for input emulation,
|
||||
which has the goal to become the general approach for emulating Input on wayland.
|
||||
## Configuration
|
||||
To automatically load clients on startup, the file `$XDG_CONFIG_HOME/lan-mouse/config.toml` is parsed.
|
||||
`$XDG_CONFIG_HOME` defaults to `~/.config/`.
|
||||
|
||||
| Required Protocols (Event Receiving) | Sway | Kwin | Gnome |
|
||||
|----------------------------------------|--------------------|----------------------|----------------------|
|
||||
| wlr-virtual-pointer-unstable-v1 | :heavy_check_mark: | :x: | :x: |
|
||||
| virtual-keyboard-unstable-v1 | :heavy_check_mark: | :x: | :x: |
|
||||
| ~fake-input~ | :x: | ~:heavy_check_mark:~ | :x: |
|
||||
To create this file you can copy the following example config:
|
||||
|
||||
### Input capture
|
||||
### Example config
|
||||
|
||||
To capture mouse and keyboard input, a few things are necessary:
|
||||
- Displaying an immovable surface at screen edges
|
||||
- Locking the mouse in place
|
||||
- (optionally but highly recommended) reading unaccelerated mouse input
|
||||
```toml
|
||||
# example configuration
|
||||
|
||||
| Required Protocols (Event Emitting) | Sway | Kwin | Gnome |
|
||||
|----------------------------------------|--------------------|----------------------|----------------------|
|
||||
| pointer-constraints-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| relative-pointer-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| keyboard-shortcuts-inhibit-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| wlr-layer-shell-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :x: |
|
||||
# optional port (defaults to 4242)
|
||||
port = 4242
|
||||
# # optional frontend -> defaults to gtk if available
|
||||
# # possible values are "cli" and "gtk"
|
||||
# frontend = "gtk"
|
||||
|
||||
The [zwlr\_virtual\_pointer\_manager\_v1](wlr-virtual-pointer-unstable-v1) is required
|
||||
to display surfaces on screen edges and used to display the immovable window on
|
||||
both wlroots based compositors and KDE.
|
||||
# define a client on the right side with host name "iridium"
|
||||
[right]
|
||||
# hostname
|
||||
host_name = "iridium"
|
||||
# optional list of (known) ip addresses
|
||||
ips = ["192.168.178.156"]
|
||||
|
||||
Gnome unfortunately does not support this protocol
|
||||
and [likely won't ever support it](https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/1141).
|
||||
# define a client on the left side with IP address 192.168.178.189
|
||||
[left]
|
||||
# The hostname is optional: When no hostname is specified,
|
||||
# at least one ip address needs to be specified.
|
||||
host_name = "thorium"
|
||||
# ips for ethernet and wifi
|
||||
ips = ["192.168.178.189", "192.168.178.172"]
|
||||
# optional port
|
||||
port = 4242
|
||||
```
|
||||
|
||||
So there is currently no way of doing this in Wayland, aside from a custom Gnome-Shell
|
||||
extension, which is not a very elegant solution.
|
||||
Where `left` can be either `left`, `right`, `top` or `bottom`.
|
||||
|
||||
This is to be looked into in the future.
|
||||
|
||||
~In order for layershell surfaces to be able to lock the pointer using the pointer\_constraints protocol [this patch](https://github.com/swaywm/sway/pull/7178) needs to be applied to sway.~
|
||||
(this works natively on sway versions >= 1.8)
|
||||
|
||||
## Windows support
|
||||
Currently windows can receive mouse and keyboard events, however unlike
|
||||
with the wlroots back-end,
|
||||
|
||||
the scancodes are not translated between keyboard layouts.
|
||||
|
||||
Event emitting is WIP.
|
||||
|
||||
|
||||
## TODOS
|
||||
- [x] Capture the actual mouse events on the server side via a wayland client and send them to the client
|
||||
- [x] Mouse grabbing
|
||||
- [x] Window with absolute position -> wlr\_layer\_shell
|
||||
- [x] DNS resolving
|
||||
- [x] Keyboard support
|
||||
- [x] Scrollwheel support
|
||||
- [x] Button support
|
||||
- [ ] Latency measurement + logging
|
||||
- [ ] Bandwidth usage approximation + logging
|
||||
- [ ] Multiple IP addresses -> check which one is reachable
|
||||
- [x] Merge server and client -> Both client and server can send and receive events depending on what mouse is used where
|
||||
- [ ] Liveness tracking (automatically ungrab mouse when client unreachable)
|
||||
## Roadmap
|
||||
- [x] Graphical frontend (gtk + libadwaita)
|
||||
- [x] respect xdg-config-home for config file location.
|
||||
- [x] IP Address switching
|
||||
- [x] Liveness tracking Automatically ungrab mouse when client unreachable
|
||||
- [ ] Liveness tracking: Automatically release keys, when server offline
|
||||
- [ ] X11 Input Capture
|
||||
- [ ] Windows Input Capture
|
||||
- [ ] MacOS Input Capture
|
||||
- [ ] MaxOS KeyCode Translation
|
||||
- [ ] Latency measurement and visualization
|
||||
- [ ] Bandwidth usage measurement and visualization
|
||||
- [ ] Clipboard support
|
||||
- [ ] Graphical frontend (gtk?)
|
||||
- [ ] *Encrytion*
|
||||
- [ ] Gnome Shell Extension (layer shell is not supported)
|
||||
- [ ] respect xdg-config-home for config file location.
|
||||
- [ ] *Encryption*
|
||||
|
||||
## Protocol
|
||||
Currently *all* mouse and keyboard events are sent via **UDP** for performance reasons.
|
||||
@@ -207,3 +267,54 @@ would be a better choice for the future and could also help for WIFI connections
|
||||
Sending key and mouse event data over the local network might not be the biggest security concern but in any public network or business environment it's *QUITE* a problem to basically broadcast your keystrokes.
|
||||
- There should be an encryption layer below the application to enable a secure link.
|
||||
- The encryption keys could be generated by the graphical frontend.
|
||||
|
||||
|
||||
## Wayland support
|
||||
### Input Emulation (for receiving events)
|
||||
On wayland input-emulation is in an early/unstable state as of writing this.
|
||||
|
||||
For this reason a suitable backend is chosen based on the active desktop environment / compositor.
|
||||
|
||||
Different compositors have different ways of enabling input emulation:
|
||||
|
||||
#### Wlroots
|
||||
Most wlroots-based compositors like Hyprland and Sway support the following
|
||||
unstable wayland protocols for keyboard and mouse emulation:
|
||||
- [virtual-keyboard-unstable-v1](https://wayland.app/protocols/virtual-keyboard-unstable-v1)
|
||||
- [wlr-virtual-pointer-unstable-v1](https://wayland.app/protocols/wlr-virtual-pointer-unstable-v1)
|
||||
|
||||
#### KDE
|
||||
KDE also has a protocol for input emulation ([kde-fake-input](https://wayland.app/protocols/kde-fake-input)),
|
||||
it is however not exposed to third party applications.
|
||||
|
||||
The recommended way to emulate input on KDE is the
|
||||
[freedesktop remote-desktop-portal](https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.RemoteDesktop).
|
||||
|
||||
#### Gnome
|
||||
Gnome uses [libei](https://gitlab.freedesktop.org/libinput/libei) for input emulation and capture,
|
||||
which has the goal to become the general approach for emulating and capturing Input on Wayland.
|
||||
|
||||
### Input capture
|
||||
|
||||
To capture mouse and keyboard input, a few things are necessary:
|
||||
- Displaying an immovable surface at screen edges
|
||||
- Locking the mouse in place
|
||||
- (optionally but highly recommended) reading unaccelerated mouse input
|
||||
|
||||
| Required Protocols (Event Emitting) | Sway | Kwin | Gnome |
|
||||
|----------------------------------------|--------------------|----------------------|----------------------|
|
||||
| pointer-constraints-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| relative-pointer-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| keyboard-shortcuts-inhibit-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| wlr-layer-shell-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :x: |
|
||||
|
||||
The [zwlr\_virtual\_pointer\_manager\_v1](wlr-virtual-pointer-unstable-v1) is required
|
||||
to display surfaces on screen edges and used to display the immovable window on
|
||||
both wlroots based compositors and KDE.
|
||||
|
||||
Gnome unfortunately does not support this protocol
|
||||
and [likely won't ever support it](https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/1141).
|
||||
|
||||
~In order for layershell surfaces to be able to lock the pointer using the pointer\_constraints protocol [this patch](https://github.com/swaywm/sway/pull/7178) needs to be applied to sway.~
|
||||
(this works natively on sway versions >= 1.8)
|
||||
|
||||
|
||||
8
build.rs
Normal file
8
build.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
fn main() {
|
||||
// composite_templates
|
||||
glib_build_tools::compile_resources(
|
||||
&["resources"],
|
||||
"resources/resources.gresource.xml",
|
||||
"lan-mouse.gresource",
|
||||
);
|
||||
}
|
||||
11
config.toml
11
config.toml
@@ -2,19 +2,22 @@
|
||||
|
||||
# optional port (defaults to 4242)
|
||||
port = 4242
|
||||
# optional frontend -> defaults to gtk if available
|
||||
# frontend = "gtk"
|
||||
|
||||
# define a client on the right side with host name "iridium"
|
||||
[right]
|
||||
# hostname
|
||||
host_name = "iridium"
|
||||
# optional list of (known) ip addresses
|
||||
ips = ["192.168.178.141"]
|
||||
# optional port (defaults to 4242)
|
||||
port = 4242
|
||||
ips = ["192.168.178.156"]
|
||||
|
||||
# define a client on the left side with IP address 192.168.178.189
|
||||
[left]
|
||||
# The hostname is optional: When no hostname is specified,
|
||||
# at least one ip address needs to be specified.
|
||||
host_name = "thorium"
|
||||
ips = ["192.168.178.189"]
|
||||
# ips for ethernet and wifi
|
||||
ips = ["192.168.178.189", "192.168.178.172"]
|
||||
# optional port
|
||||
port = 4242
|
||||
|
||||
14
de.feschber.LanMouse.yml
Normal file
14
de.feschber.LanMouse.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
app-id: de.feschber.LanMouse
|
||||
runtime: org.freedesktop.Platform
|
||||
runtime-version: '22.08'
|
||||
sdk: org.freedesktop.Sdk
|
||||
command: target/release/lan-mouse
|
||||
modules:
|
||||
- name: hello
|
||||
buildsystem: simple
|
||||
build-commands:
|
||||
- cargo build --release
|
||||
- install -D lan-mouse /app/bin/lan-mouse
|
||||
sources:
|
||||
- type: file
|
||||
path: target/release/lan-mouse
|
||||
13
lan-mouse.desktop
Normal file
13
lan-mouse.desktop
Normal file
@@ -0,0 +1,13 @@
|
||||
[Desktop Entry]
|
||||
Categories=Utility;
|
||||
Comment[en_US]=mouse & keyboard sharing via LAN
|
||||
Comment[de_DE]=Maus- und Tastaturfreigabe über LAN
|
||||
Exec=lan-mouse
|
||||
Icon=mouse-icon.svg
|
||||
Name[en_US]=Lan Mouse
|
||||
Name[de_DE]=Lan Maus
|
||||
Name=Lan Mouse
|
||||
StartupNotify=true
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Version=0.5.0
|
||||
74
resources/client_row.ui
Normal file
74
resources/client_row.ui
Normal file
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<template class="ClientRow" parent="AdwExpanderRow">
|
||||
<property name="title">hostname</property>
|
||||
<!-- enabled -->
|
||||
<child type="prefix">
|
||||
<object class="GtkSwitch" id="enable_switch">
|
||||
<signal name="state_set" handler="handle_client_set_state" swapped="true"/>
|
||||
<property name="valign">center</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="tooltip-text" translatable="yes">enable</property>
|
||||
</object>
|
||||
</child>
|
||||
<!-- host -->
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="title">hostname</property>
|
||||
<property name="subtitle">port</property>
|
||||
<!-- hostname -->
|
||||
<child>
|
||||
<object class="GtkEntry" id="hostname">
|
||||
<!-- <property name="title" translatable="yes">hostname</property> -->
|
||||
<property name="xalign">0.5</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="placeholder-text">hostname</property>
|
||||
<property name="width-chars">-1</property>
|
||||
</object>
|
||||
</child>
|
||||
<!-- port -->
|
||||
<child>
|
||||
<object class="GtkEntry" id="port">
|
||||
<!-- <property name="title" translatable="yes">port</property> -->
|
||||
<property name="input_purpose">GTK_INPUT_PURPOSE_NUMBER</property>
|
||||
<property name="xalign">0.5</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="placeholder-text">4242</property>
|
||||
<property name="width-chars">5</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<!-- position -->
|
||||
<child>
|
||||
<object class="AdwComboRow" id="position">
|
||||
<property name="title" translatable="yes">position</property>
|
||||
<property name="model">
|
||||
<object class="GtkStringList">
|
||||
<items>
|
||||
<item>Left</item>
|
||||
<item>Right</item>
|
||||
<item>Top</item>
|
||||
<item>Bottom</item>
|
||||
</items>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<!-- delete button -->
|
||||
<child>
|
||||
<object class="AdwActionRow" id="delete_row">
|
||||
<property name="title">delete this client</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="delete_button">
|
||||
<signal name="activate" handler="handle_client_delete" object="delete_row" swapped="true"/>
|
||||
<property name="icon-name">user-trash-symbolic</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="name">delete-button</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
||||
171
resources/mouse-icon.svg
Normal file
171
resources/mouse-icon.svg
Normal file
@@ -0,0 +1,171 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 12.7 12.7"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
|
||||
sodipodi:docname="mouse-icon.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="22.737887"
|
||||
inkscape:cx="19.54887"
|
||||
inkscape:cy="26.167778"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1374"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<g
|
||||
id="g20"
|
||||
transform="translate(1.1586889,0.39019296)">
|
||||
<g
|
||||
id="g8"
|
||||
transform="translate(-0.11519282,-3.9659242)">
|
||||
<g
|
||||
id="g6"
|
||||
transform="translate(0.67275315,0.39959697)">
|
||||
<g
|
||||
id="g5">
|
||||
<rect
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
id="rect4"
|
||||
width="1.3032579"
|
||||
height="1.3032579"
|
||||
x="1.7199994"
|
||||
y="7.5408325"
|
||||
ry="0.3373504" />
|
||||
<rect
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
id="rect4-2"
|
||||
width="1.3032579"
|
||||
height="1.3032579"
|
||||
x="3.8428385"
|
||||
y="7.5408325"
|
||||
ry="0.3373504" />
|
||||
</g>
|
||||
<rect
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
id="rect4-3"
|
||||
width="1.3032579"
|
||||
height="1.3032579"
|
||||
x="2.781419"
|
||||
y="5.1382394"
|
||||
ry="0.3373504" />
|
||||
</g>
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
d="M 1.1519282,7.3907619 H 7.059674"
|
||||
id="path5" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
d="M 4.1058009,6.8410941 V 7.3907617"
|
||||
id="path6" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
d="m 5.1672204,7.9404294 2e-7,-0.5496677"
|
||||
id="path7" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
d="M 3.0443815,7.9404294 V 7.3907617"
|
||||
id="path8" />
|
||||
</g>
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
d="M 6.9444811,3.4248375 Z"
|
||||
id="path9" />
|
||||
<path
|
||||
style="color:#000000;fill:#000000;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
|
||||
d="m 6.9840449,3.4464199 c -0.072714,-0.0035 -0.1209639,-0.2113583 -0.125,-0.1386718 -0.0035,0.072714 0.052314,0.1346357 0.125,0.1386718 0,0 0.6614057,0.034643 1.3535156,0.4765625 0.6921097,0.4419191 1.4111567,1.2803292 1.5136717,2.9433594 0.05132,0.832563 -0.07521,1.3855916 -0.279297,1.75 -0.20409,0.3644084 -0.482943,0.5482749 -0.777343,0.640625 -0.5888014,0.1847002 -1.2265629,-0.021484 -1.2265629,-0.021484 -0.069024,-0.023541 -0.144095,0.013122 -0.1679688,0.082031 -0.023366,0.069587 0.014295,0.1449093 0.083984,0.1679687 0,0 0.6961634,0.2406696 1.3886717,0.023437 C 9.2189712,9.4003039 9.5672292,9.1706004 9.8043572,8.7472012 10.041486,8.323802 10.170261,7.7150888 10.116858,6.8487637 10.009921,5.1140179 9.2320232,4.3532014 8.4801387,3.8731154 7.7282538,3.3930294 6.9840449,3.4464198 6.9840449,3.4464199 Z"
|
||||
id="path18"
|
||||
sodipodi:nodetypes="cccsssscccssssc" />
|
||||
<g
|
||||
id="g19"
|
||||
transform="matrix(1.8148709,0,0,1.8148709,-4.1533763,-7.8818885)">
|
||||
<g
|
||||
id="g17"
|
||||
transform="translate(0.01163623,0.23038484)">
|
||||
<ellipse
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
id="path10"
|
||||
cx="3.9823804"
|
||||
cy="8.17869"
|
||||
rx="0.49368349"
|
||||
ry="0.62533247" />
|
||||
<ellipse
|
||||
style="fill:#3d3d3d;fill-opacity:1;stroke:none;stroke-width:0.168876;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
id="ellipse17"
|
||||
cx="3.9823804"
|
||||
cy="8.17869"
|
||||
rx="0.31096464"
|
||||
ry="0.40317491" />
|
||||
</g>
|
||||
<path
|
||||
id="path11"
|
||||
style="stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round"
|
||||
d="M 7.479305,9.4704944 C 7.4964603,9.9336885 6.9306558,9.9678313 5.3811502,10.087599 3.2109768,10.255341 2.4751992,9.6707727 2.4355055,9.5280908 2.3112754,9.0815374 3.8270232,8.4090748 5.3811502,8.4090748 c 1.5633309,0 2.0816988,0.6171052 2.0981548,1.0614196 z"
|
||||
sodipodi:nodetypes="sssss" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
id="path12"
|
||||
cx="3.5281858"
|
||||
cy="9.0632057"
|
||||
r="0.18513133" />
|
||||
<g
|
||||
id="g18"
|
||||
transform="translate(0.01163623,0.23038484)">
|
||||
<ellipse
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
id="path10-2"
|
||||
cx="4.6085634"
|
||||
cy="8.17869"
|
||||
rx="0.49368349"
|
||||
ry="0.62533247" />
|
||||
<ellipse
|
||||
style="fill:#3d3d3d;fill-opacity:1;stroke:none;stroke-width:0.168876;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
id="ellipse16"
|
||||
cx="4.6085634"
|
||||
cy="8.17869"
|
||||
rx="0.31096464"
|
||||
ry="0.40317491" />
|
||||
</g>
|
||||
<ellipse
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.112226;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
id="circle18"
|
||||
cx="3.5003331"
|
||||
cy="9.0344076"
|
||||
rx="0.078639306"
|
||||
ry="0.07816644" />
|
||||
<ellipse
|
||||
style="fill:#4f4f4f;fill-opacity:1;stroke:none;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="path19"
|
||||
cx="2.4818404"
|
||||
cy="9.4499254"
|
||||
rx="0.05348238"
|
||||
ry="0.11930636" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.6 KiB |
12
resources/resources.gresource.xml
Normal file
12
resources/resources.gresource.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gresources>
|
||||
<gresource prefix="/de/feschber/LanMouse">
|
||||
<file compressed="true" preprocess="xml-stripblanks">window.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">client_row.ui</file>
|
||||
<file compressed="true">style.css</file>
|
||||
<file compressed="true">style-dark.css</file>
|
||||
</gresource>
|
||||
<gresource prefix="/de/feschber/LanMouse/icons">
|
||||
<file compressed="true" preprocess="xml-stripblanks">mouse-icon.svg</file>
|
||||
</gresource>
|
||||
</gresources>
|
||||
11
resources/style-dark.css
Normal file
11
resources/style-dark.css
Normal file
@@ -0,0 +1,11 @@
|
||||
#delete-button {
|
||||
color: @red_1;
|
||||
}
|
||||
|
||||
#port-edit-cancel {
|
||||
color: @red_1;
|
||||
}
|
||||
|
||||
#port-edit-apply {
|
||||
color: @green_1;
|
||||
}
|
||||
11
resources/style.css
Normal file
11
resources/style.css
Normal file
@@ -0,0 +1,11 @@
|
||||
#delete-button {
|
||||
color: @red_3;
|
||||
}
|
||||
|
||||
#port-edit-cancel {
|
||||
color: @red_3;
|
||||
}
|
||||
|
||||
#port-edit-apply {
|
||||
color: @green_3;
|
||||
}
|
||||
146
resources/window.ui
Normal file
146
resources/window.ui
Normal file
@@ -0,0 +1,146 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="libadwaita" version="1.0"/>
|
||||
<menu id="main-menu">
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Close window</attribute>
|
||||
<attribute name="action">window.close</attribute>
|
||||
</item>
|
||||
</menu>
|
||||
<template class="LanMouseWindow" parent="AdwApplicationWindow">
|
||||
<property name="width-request">600</property>
|
||||
<property name="height-request">700</property>
|
||||
<property name="title" translatable="yes">Lan Mouse</property>
|
||||
<property name="show-menubar">True</property>
|
||||
<property name="content">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child type="top">
|
||||
<object class="AdwHeaderBar">
|
||||
<child type ="end">
|
||||
<object class="GtkMenuButton">
|
||||
<property name="icon-name">open-menu-symbolic</property>
|
||||
<property name="menu-model">main-menu</property>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="flat"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwToastOverlay" id="toast_overlay">
|
||||
<child>
|
||||
<object class="AdwStatusPage">
|
||||
<property name="title" translatable="yes">Lan Mouse</property>
|
||||
<property name="description" translatable="yes">easily use your mouse and keyboard on multiple computers</property>
|
||||
<property name="icon-name">mouse-icon</property>
|
||||
<property name="child">
|
||||
<object class="AdwClamp">
|
||||
<property name="maximum-size">600</property>
|
||||
<property name="tightening-threshold">0</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">12</property>
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup">
|
||||
<property name="title" translatable="yes">General</property>
|
||||
<!--
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="title" translatable="yes">enable</property>
|
||||
<child type="suffix">
|
||||
<object class="GtkSwitch">
|
||||
<property name="valign">center</property>
|
||||
<property name="tooltip-text" translatable="yes">enable</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
-->
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="title">port</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="port_entry">
|
||||
<signal name="activate" handler="handle_port_edit_apply" swapped="true"/>
|
||||
<signal name="changed" handler="handle_port_changed" swapped="true"/>
|
||||
<!-- <signal name="delete-text" handler="handle_port_changed" swapped="true"/> -->
|
||||
<!-- <property name="title" translatable="yes">port</property> -->
|
||||
<property name="placeholder-text">4242</property>
|
||||
<property name="width-chars">5</property>
|
||||
<property name="xalign">0.5</property>
|
||||
<property name="valign">center</property>
|
||||
<!-- <property name="show-apply-button">True</property> -->
|
||||
<property name="input-purpose">GTK_INPUT_PURPOSE_DIGITS</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="port_edit_apply">
|
||||
<signal name="clicked" handler="handle_port_edit_apply" swapped="true"/>
|
||||
<property name="icon-name">object-select-symbolic</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="visible">false</property>
|
||||
<property name="name">port-edit-apply</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="port_edit_cancel">
|
||||
<signal name="clicked" handler="handle_port_edit_cancel" swapped="true"/>
|
||||
<property name="icon-name">process-stop-symbolic</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="visible">false</property>
|
||||
<property name="name">port-edit-cancel</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup">
|
||||
<property name="title" translatable="yes">Connections</property>
|
||||
<property name="header-suffix">
|
||||
<object class="GtkButton">
|
||||
<signal name="clicked" handler="handle_add_client_pressed" swapped="true"/>
|
||||
<property name="child">
|
||||
<object class="AdwButtonContent">
|
||||
<property name="icon-name">list-add-symbolic</property>
|
||||
<property name="label" translatable="yes">Add</property>
|
||||
</object>
|
||||
</property>
|
||||
<style>
|
||||
<class name="flat"/>
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
<child>
|
||||
<object class="GtkListBox" id="client_list">
|
||||
<property name="selection-mode">none</property>
|
||||
<child type="placeholder">
|
||||
<object class="AdwActionRow" id="client_placeholder">
|
||||
<property name="title">No connections!</property>
|
||||
<property name="subtitle">add a new client via the + button</property>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="boxed-list" />
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</template>
|
||||
</interface>
|
||||
@@ -1,14 +1,20 @@
|
||||
#[cfg(windows)]
|
||||
pub mod windows;
|
||||
|
||||
#[cfg(all(unix, feature="x11"))]
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
pub mod x11;
|
||||
|
||||
#[cfg(all(unix, feature = "wayland"))]
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
pub mod wlroots;
|
||||
|
||||
#[cfg(all(unix, feature = "xdg_desktop_portal"))]
|
||||
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
|
||||
pub mod xdg_desktop_portal;
|
||||
|
||||
#[cfg(all(unix, feature = "libei"))]
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
pub mod libei;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod macos;
|
||||
|
||||
/// fallback consumer
|
||||
pub mod dummy;
|
||||
|
||||
26
src/backend/consumer/dummy.rs
Normal file
26
src/backend/consumer/dummy.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use crate::{
|
||||
client::{ClientEvent, ClientHandle},
|
||||
consumer::EventConsumer,
|
||||
event::Event,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DummyConsumer;
|
||||
|
||||
impl DummyConsumer {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventConsumer for DummyConsumer {
|
||||
async fn consume(&mut self, event: Event, client_handle: ClientHandle) {
|
||||
log::info!("received event: ({client_handle}) {event}");
|
||||
}
|
||||
async fn notify(&mut self, client_event: ClientEvent) {
|
||||
log::info!("{client_event:?}");
|
||||
}
|
||||
async fn destroy(&mut self) {}
|
||||
}
|
||||
@@ -1,18 +1,381 @@
|
||||
use crate::consumer::EventConsumer;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io,
|
||||
os::{
|
||||
fd::{FromRawFd, RawFd},
|
||||
unix::net::UnixStream,
|
||||
},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
pub struct LibeiConsumer {}
|
||||
use anyhow::{anyhow, Result};
|
||||
use ashpd::desktop::remote_desktop::{DeviceType, RemoteDesktop};
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
|
||||
use reis::{
|
||||
ei::{self, button::ButtonState, handshake::ContextType, keyboard::KeyState},
|
||||
tokio::EiEventStream,
|
||||
PendingRequestResult,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
client::{ClientEvent, ClientHandle},
|
||||
consumer::EventConsumer,
|
||||
event::Event,
|
||||
};
|
||||
|
||||
pub struct LibeiConsumer {
|
||||
handshake: bool,
|
||||
context: ei::Context,
|
||||
events: EiEventStream,
|
||||
pointer: Option<(ei::Device, ei::Pointer)>,
|
||||
has_pointer: bool,
|
||||
scroll: Option<(ei::Device, ei::Scroll)>,
|
||||
has_scroll: bool,
|
||||
button: Option<(ei::Device, ei::Button)>,
|
||||
has_button: bool,
|
||||
keyboard: Option<(ei::Device, ei::Keyboard)>,
|
||||
has_keyboard: bool,
|
||||
capabilities: HashMap<String, u64>,
|
||||
capability_mask: u64,
|
||||
sequence: u32,
|
||||
serial: u32,
|
||||
}
|
||||
|
||||
async fn get_ei_fd() -> Result<RawFd, 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?;
|
||||
|
||||
proxy
|
||||
.start(&session, &ashpd::WindowIdentifier::default())
|
||||
.await?
|
||||
.response()?;
|
||||
proxy.connect_to_eis(&session).await
|
||||
}
|
||||
|
||||
impl LibeiConsumer {
|
||||
pub fn new() -> Self { Self { } }
|
||||
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::connect("/run/user/1000/eis-0")?;
|
||||
stream.set_nonblocking(true)?;
|
||||
let context = ei::Context::new(stream)?;
|
||||
context.flush()?;
|
||||
let events = EiEventStream::new(context.clone())?;
|
||||
Ok(Self {
|
||||
handshake: false,
|
||||
context,
|
||||
events,
|
||||
pointer: None,
|
||||
button: None,
|
||||
scroll: None,
|
||||
keyboard: None,
|
||||
has_pointer: false,
|
||||
has_button: false,
|
||||
has_scroll: false,
|
||||
has_keyboard: false,
|
||||
capabilities: HashMap::new(),
|
||||
capability_mask: 0,
|
||||
sequence: 0,
|
||||
serial: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventConsumer for LibeiConsumer {
|
||||
fn consume(&self, _: crate::event::Event, _: crate::client::ClientHandle) {
|
||||
log::error!("libei backend not yet implemented!");
|
||||
todo!()
|
||||
async fn consume(&mut self, event: Event, _client_handle: ClientHandle) {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_micros() as u64;
|
||||
match event {
|
||||
Event::Pointer(p) => match p {
|
||||
crate::event::PointerEvent::Motion {
|
||||
time: _,
|
||||
relative_x,
|
||||
relative_y,
|
||||
} => {
|
||||
if !self.has_pointer {
|
||||
return;
|
||||
}
|
||||
if let Some((d, p)) = self.pointer.as_mut() {
|
||||
p.motion_relative(relative_x as f32, relative_y as f32);
|
||||
d.frame(self.serial, now);
|
||||
}
|
||||
}
|
||||
crate::event::PointerEvent::Button {
|
||||
time: _,
|
||||
button,
|
||||
state,
|
||||
} => {
|
||||
if !self.has_button {
|
||||
return;
|
||||
}
|
||||
if let Some((d, b)) = self.button.as_mut() {
|
||||
b.button(
|
||||
button,
|
||||
match state {
|
||||
0 => ButtonState::Released,
|
||||
_ => ButtonState::Press,
|
||||
},
|
||||
);
|
||||
d.frame(self.serial, now);
|
||||
}
|
||||
}
|
||||
crate::event::PointerEvent::Axis {
|
||||
time: _,
|
||||
axis,
|
||||
value,
|
||||
} => {
|
||||
if !self.has_scroll {
|
||||
return;
|
||||
}
|
||||
if let Some((d, s)) = self.scroll.as_mut() {
|
||||
match axis {
|
||||
0 => s.scroll(0., value as f32),
|
||||
_ => s.scroll(value as f32, 0.),
|
||||
}
|
||||
d.frame(self.serial, now);
|
||||
}
|
||||
}
|
||||
crate::event::PointerEvent::Frame {} => {}
|
||||
},
|
||||
Event::Keyboard(k) => match k {
|
||||
crate::event::KeyboardEvent::Key {
|
||||
time: _,
|
||||
key,
|
||||
state,
|
||||
} => {
|
||||
if !self.has_keyboard {
|
||||
return;
|
||||
}
|
||||
if let Some((d, k)) = &mut self.keyboard {
|
||||
k.key(
|
||||
key,
|
||||
match state {
|
||||
0 => KeyState::Released,
|
||||
_ => KeyState::Press,
|
||||
},
|
||||
);
|
||||
d.frame(self.serial, now);
|
||||
}
|
||||
}
|
||||
crate::event::KeyboardEvent::Modifiers { .. } => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
self.context.flush().unwrap();
|
||||
}
|
||||
|
||||
fn notify(&mut self, _: crate::client::ClientEvent) {
|
||||
todo!()
|
||||
async fn dispatch(&mut self) -> Result<()> {
|
||||
let event = match self.events.next().await {
|
||||
Some(e) => e?,
|
||||
None => return Err(anyhow!("libei connection lost")),
|
||||
};
|
||||
let event = match event {
|
||||
PendingRequestResult::Request(result) => result,
|
||||
PendingRequestResult::ParseError(e) => {
|
||||
return Err(anyhow!("libei protocol violation: {e}"))
|
||||
}
|
||||
PendingRequestResult::InvalidObject(e) => return Err(anyhow!("invalid object {e}")),
|
||||
};
|
||||
match event {
|
||||
ei::Event::Handshake(handshake, request) => match request {
|
||||
ei::handshake::Event::HandshakeVersion { version } => {
|
||||
if self.handshake {
|
||||
return Ok(());
|
||||
}
|
||||
log::info!("libei version {}", version);
|
||||
// sender means we are sending events _to_ the eis server
|
||||
handshake.handshake_version(version); // FIXME
|
||||
handshake.context_type(ContextType::Sender);
|
||||
handshake.name("ei-demo-client");
|
||||
handshake.interface_version("ei_connection", 1);
|
||||
handshake.interface_version("ei_callback", 1);
|
||||
handshake.interface_version("ei_pingpong", 1);
|
||||
handshake.interface_version("ei_seat", 1);
|
||||
handshake.interface_version("ei_device", 2);
|
||||
handshake.interface_version("ei_pointer", 1);
|
||||
handshake.interface_version("ei_pointer_absolute", 1);
|
||||
handshake.interface_version("ei_scroll", 1);
|
||||
handshake.interface_version("ei_button", 1);
|
||||
handshake.interface_version("ei_keyboard", 1);
|
||||
handshake.interface_version("ei_touchscreen", 1);
|
||||
handshake.finish();
|
||||
self.handshake = true;
|
||||
}
|
||||
ei::handshake::Event::InterfaceVersion { name, version } => {
|
||||
log::debug!("handshake: Interface {name} @ {version}");
|
||||
}
|
||||
ei::handshake::Event::Connection { serial, connection } => {
|
||||
connection.sync(1);
|
||||
self.serial = serial;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
ei::Event::Connection(_connection, request) => match request {
|
||||
ei::connection::Event::Seat { seat } => {
|
||||
log::debug!("connected to seat: {seat:?}");
|
||||
}
|
||||
ei::connection::Event::Ping { ping } => {
|
||||
ping.done(0);
|
||||
}
|
||||
ei::connection::Event::Disconnected {
|
||||
last_serial: _,
|
||||
reason,
|
||||
explanation,
|
||||
} => {
|
||||
log::debug!("ei - disconnected: reason: {reason:?}: {explanation}")
|
||||
}
|
||||
ei::connection::Event::InvalidObject {
|
||||
last_serial,
|
||||
invalid_id,
|
||||
} => {
|
||||
return Err(anyhow!(
|
||||
"invalid object: id: {invalid_id}, serial: {last_serial}"
|
||||
));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
ei::Event::Device(device, request) => match request {
|
||||
ei::device::Event::Destroyed { serial } => {
|
||||
log::debug!("device destroyed: {device:?} - serial: {serial}")
|
||||
}
|
||||
ei::device::Event::Name { name } => {
|
||||
log::debug!("device name: {name}")
|
||||
}
|
||||
ei::device::Event::DeviceType { device_type } => {
|
||||
log::debug!("device type: {device_type:?}")
|
||||
}
|
||||
ei::device::Event::Dimensions { width, height } => {
|
||||
log::debug!("device dimensions: {width}x{height}")
|
||||
}
|
||||
ei::device::Event::Region {
|
||||
offset_x,
|
||||
offset_y,
|
||||
width,
|
||||
hight,
|
||||
scale,
|
||||
} => log::debug!(
|
||||
"device region: {width}x{hight} @ ({offset_x},{offset_y}), scale: {scale}"
|
||||
),
|
||||
ei::device::Event::Interface { object } => {
|
||||
log::debug!("device interface: {object:?}");
|
||||
if object.interface().eq("ei_pointer") {
|
||||
log::debug!("GOT POINTER DEVICE");
|
||||
self.pointer.replace((device, object.downcast().unwrap()));
|
||||
} else if object.interface().eq("ei_button") {
|
||||
log::debug!("GOT BUTTON DEVICE");
|
||||
self.button.replace((device, object.downcast().unwrap()));
|
||||
} else if object.interface().eq("ei_scroll") {
|
||||
log::debug!("GOT SCROLL DEVICE");
|
||||
self.scroll.replace((device, object.downcast().unwrap()));
|
||||
} else if object.interface().eq("ei_keyboard") {
|
||||
log::debug!("GOT KEYBOARD DEVICE");
|
||||
self.keyboard.replace((device, object.downcast().unwrap()));
|
||||
}
|
||||
}
|
||||
ei::device::Event::Done => {
|
||||
log::debug!("device: done {device:?}");
|
||||
}
|
||||
ei::device::Event::Resumed { serial } => {
|
||||
self.serial = serial;
|
||||
device.start_emulating(serial, self.sequence);
|
||||
self.sequence += 1;
|
||||
log::debug!("resumed: {device:?}");
|
||||
if let Some((d, _)) = &mut self.pointer {
|
||||
if d == &device {
|
||||
log::debug!("pointer resumed {serial}");
|
||||
self.has_pointer = true;
|
||||
}
|
||||
}
|
||||
if let Some((d, _)) = &mut self.button {
|
||||
if d == &device {
|
||||
log::debug!("button resumed {serial}");
|
||||
self.has_button = true;
|
||||
}
|
||||
}
|
||||
if let Some((d, _)) = &mut self.scroll {
|
||||
if d == &device {
|
||||
log::debug!("scroll resumed {serial}");
|
||||
self.has_scroll = true;
|
||||
}
|
||||
}
|
||||
if let Some((d, _)) = &mut self.keyboard {
|
||||
if d == &device {
|
||||
log::debug!("keyboard resumed {serial}");
|
||||
self.has_keyboard = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ei::device::Event::Paused { serial } => {
|
||||
self.has_pointer = false;
|
||||
self.has_button = false;
|
||||
self.serial = serial;
|
||||
}
|
||||
ei::device::Event::StartEmulating { serial, sequence } => {
|
||||
log::debug!("start emulating {serial}, {sequence}")
|
||||
}
|
||||
ei::device::Event::StopEmulating { serial } => {
|
||||
log::debug!("stop emulating {serial}")
|
||||
}
|
||||
ei::device::Event::Frame { serial, timestamp } => {
|
||||
log::debug!("frame: {serial}, {timestamp}");
|
||||
}
|
||||
ei::device::Event::RegionMappingId { mapping_id } => {
|
||||
log::debug!("RegionMappingId {mapping_id}")
|
||||
}
|
||||
e => log::debug!("invalid event: {e:?}"),
|
||||
},
|
||||
ei::Event::Seat(seat, request) => match request {
|
||||
ei::seat::Event::Destroyed { serial } => {
|
||||
self.serial = serial;
|
||||
log::debug!("seat destroyed: {seat:?}");
|
||||
}
|
||||
ei::seat::Event::Name { name } => {
|
||||
log::debug!("seat name: {name}");
|
||||
}
|
||||
ei::seat::Event::Capability { mask, interface } => {
|
||||
log::debug!("seat capabilities: {mask}, interface: {interface:?}");
|
||||
self.capabilities.insert(interface, mask);
|
||||
self.capability_mask |= mask;
|
||||
}
|
||||
ei::seat::Event::Done => {
|
||||
log::debug!("seat done");
|
||||
log::debug!("binding capabilities: {}", self.capability_mask);
|
||||
seat.bind(self.capability_mask);
|
||||
}
|
||||
ei::seat::Event::Device { device } => {
|
||||
log::debug!("seat: new device - {device:?}");
|
||||
}
|
||||
_ => todo!(),
|
||||
},
|
||||
e => log::debug!("unhandled event: {e:?}"),
|
||||
}
|
||||
self.context.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn notify(&mut self, _client_event: ClientEvent) {}
|
||||
|
||||
async fn destroy(&mut self) {}
|
||||
}
|
||||
|
||||
238
src/backend/consumer/macos.rs
Normal file
238
src/backend/consumer/macos.rs
Normal file
@@ -0,0 +1,238 @@
|
||||
use crate::client::{ClientEvent, ClientHandle};
|
||||
use crate::consumer::EventConsumer;
|
||||
use crate::event::{Event, KeyboardEvent, PointerEvent};
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use core_graphics::display::{CGDisplayBounds, CGMainDisplayID, CGPoint};
|
||||
use core_graphics::event::{
|
||||
CGEvent, CGEventTapLocation, CGEventType, CGMouseButton, EventField, ScrollEventUnit,
|
||||
};
|
||||
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
|
||||
use std::ops::{Index, IndexMut};
|
||||
|
||||
pub struct MacOSConsumer {
|
||||
pub event_source: CGEventSource,
|
||||
button_state: ButtonState,
|
||||
}
|
||||
|
||||
struct ButtonState {
|
||||
left: bool,
|
||||
right: bool,
|
||||
center: bool,
|
||||
}
|
||||
|
||||
impl Index<CGMouseButton> for ButtonState {
|
||||
type Output = bool;
|
||||
|
||||
fn index(&self, index: CGMouseButton) -> &Self::Output {
|
||||
match index {
|
||||
CGMouseButton::Left => &self.left,
|
||||
CGMouseButton::Right => &self.right,
|
||||
CGMouseButton::Center => &self.center,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexMut<CGMouseButton> for ButtonState {
|
||||
fn index_mut(&mut self, index: CGMouseButton) -> &mut Self::Output {
|
||||
match index {
|
||||
CGMouseButton::Left => &mut self.left,
|
||||
CGMouseButton::Right => &mut self.right,
|
||||
CGMouseButton::Center => &mut self.center,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for MacOSConsumer {}
|
||||
|
||||
impl MacOSConsumer {
|
||||
pub fn new() -> Result<Self> {
|
||||
let event_source = match CGEventSource::new(CGEventSourceStateID::CombinedSessionState) {
|
||||
Ok(e) => e,
|
||||
Err(_) => return Err(anyhow!("event source creation failed!")),
|
||||
};
|
||||
let button_state = ButtonState {
|
||||
left: false,
|
||||
right: false,
|
||||
center: false,
|
||||
};
|
||||
Ok(Self {
|
||||
event_source,
|
||||
button_state,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_mouse_location(&self) -> Option<CGPoint> {
|
||||
let event: CGEvent = CGEvent::new(self.event_source.clone()).ok()?;
|
||||
Some(event.location())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventConsumer for MacOSConsumer {
|
||||
async fn consume(&mut self, event: Event, _client_handle: ClientHandle) {
|
||||
match event {
|
||||
Event::Pointer(pointer_event) => match pointer_event {
|
||||
PointerEvent::Motion {
|
||||
time: _,
|
||||
relative_x,
|
||||
relative_y,
|
||||
} => {
|
||||
// FIXME secondary displays?
|
||||
let (min_x, min_y, max_x, max_y) = unsafe {
|
||||
let display = CGMainDisplayID();
|
||||
let bounds = CGDisplayBounds(display);
|
||||
let min_x = bounds.origin.x;
|
||||
let max_x = bounds.origin.x + bounds.size.width;
|
||||
let min_y = bounds.origin.y;
|
||||
let max_y = bounds.origin.y + bounds.size.height;
|
||||
(min_x as f64, min_y as f64, max_x as f64, max_y as f64)
|
||||
};
|
||||
let mut mouse_location = match self.get_mouse_location() {
|
||||
Some(l) => l,
|
||||
None => {
|
||||
log::warn!("could not get mouse location!");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
mouse_location.x = (mouse_location.x + relative_x).clamp(min_x, max_x - 1.);
|
||||
mouse_location.y = (mouse_location.y + relative_y).clamp(min_y, max_y - 1.);
|
||||
|
||||
let mut event_type = CGEventType::MouseMoved;
|
||||
if self.button_state.left {
|
||||
event_type = CGEventType::LeftMouseDragged
|
||||
} else if self.button_state.right {
|
||||
event_type = CGEventType::RightMouseDragged
|
||||
} else if self.button_state.center {
|
||||
event_type = CGEventType::OtherMouseDragged
|
||||
};
|
||||
let event = match CGEvent::new_mouse_event(
|
||||
self.event_source.clone(),
|
||||
event_type,
|
||||
mouse_location,
|
||||
CGMouseButton::Left,
|
||||
) {
|
||||
Ok(e) => e,
|
||||
Err(_) => {
|
||||
log::warn!("mouse event creation failed!");
|
||||
return;
|
||||
}
|
||||
};
|
||||
event.set_integer_value_field(
|
||||
EventField::MOUSE_EVENT_DELTA_X,
|
||||
relative_x as i64,
|
||||
);
|
||||
event.set_integer_value_field(
|
||||
EventField::MOUSE_EVENT_DELTA_Y,
|
||||
relative_y as i64,
|
||||
);
|
||||
event.post(CGEventTapLocation::HID);
|
||||
}
|
||||
PointerEvent::Button {
|
||||
time: _,
|
||||
button,
|
||||
state,
|
||||
} => {
|
||||
let (event_type, mouse_button) = match (button, state) {
|
||||
(b, 1) if b == crate::event::BTN_LEFT => {
|
||||
(CGEventType::LeftMouseDown, CGMouseButton::Left)
|
||||
}
|
||||
(b, 0) if b == crate::event::BTN_LEFT => {
|
||||
(CGEventType::LeftMouseUp, CGMouseButton::Left)
|
||||
}
|
||||
(b, 1) if b == crate::event::BTN_RIGHT => {
|
||||
(CGEventType::RightMouseDown, CGMouseButton::Right)
|
||||
}
|
||||
(b, 0) if b == crate::event::BTN_RIGHT => {
|
||||
(CGEventType::RightMouseUp, CGMouseButton::Right)
|
||||
}
|
||||
(b, 1) if b == crate::event::BTN_MIDDLE => {
|
||||
(CGEventType::OtherMouseDown, CGMouseButton::Center)
|
||||
}
|
||||
(b, 0) if b == crate::event::BTN_MIDDLE => {
|
||||
(CGEventType::OtherMouseUp, CGMouseButton::Center)
|
||||
}
|
||||
_ => {
|
||||
log::warn!("invalid button event: {button},{state}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
// store button state
|
||||
self.button_state[mouse_button] = state == 1;
|
||||
|
||||
let location = self.get_mouse_location().unwrap();
|
||||
let event = match CGEvent::new_mouse_event(
|
||||
self.event_source.clone(),
|
||||
event_type,
|
||||
location,
|
||||
mouse_button,
|
||||
) {
|
||||
Ok(e) => e,
|
||||
Err(()) => {
|
||||
log::warn!("mouse event creation failed!");
|
||||
return;
|
||||
}
|
||||
};
|
||||
event.post(CGEventTapLocation::HID);
|
||||
}
|
||||
PointerEvent::Axis {
|
||||
time: _,
|
||||
axis,
|
||||
value,
|
||||
} => {
|
||||
let value = value as i32 / 10; // FIXME: high precision scroll events
|
||||
let (count, wheel1, wheel2, wheel3) = match axis {
|
||||
0 => (1, value, 0, 0), // 0 = vertical => 1 scroll wheel device (y axis)
|
||||
1 => (2, 0, value, 0), // 1 = horizontal => 2 scroll wheel devices (y, x) -> (0, x)
|
||||
_ => {
|
||||
log::warn!("invalid scroll event: {axis}, {value}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let event = match CGEvent::new_scroll_event(
|
||||
self.event_source.clone(),
|
||||
ScrollEventUnit::LINE,
|
||||
count,
|
||||
wheel1,
|
||||
wheel2,
|
||||
wheel3,
|
||||
) {
|
||||
Ok(e) => e,
|
||||
Err(()) => {
|
||||
log::warn!("scroll event creation failed!");
|
||||
return;
|
||||
}
|
||||
};
|
||||
event.post(CGEventTapLocation::HID);
|
||||
}
|
||||
PointerEvent::Frame { .. } => {}
|
||||
},
|
||||
Event::Keyboard(keyboard_event) => match keyboard_event {
|
||||
KeyboardEvent::Key { .. } => {
|
||||
/*
|
||||
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,
|
||||
Err(_) => {
|
||||
log::warn!("unable to create key event");
|
||||
return
|
||||
}
|
||||
};
|
||||
event.post(CGEventTapLocation::HID);
|
||||
*/
|
||||
}
|
||||
KeyboardEvent::Modifiers { .. } => {}
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
async fn notify(&mut self, _client_event: ClientEvent) {}
|
||||
|
||||
async fn destroy(&mut self) {}
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
use crate::{event::{KeyboardEvent, PointerEvent}, consumer::EventConsumer};
|
||||
use crate::{
|
||||
consumer::EventConsumer,
|
||||
event::{KeyboardEvent, PointerEvent},
|
||||
scancode,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use winapi::um::winuser::{SendInput, KEYEVENTF_EXTENDEDKEY};
|
||||
use winapi::{
|
||||
self,
|
||||
um::winuser::{INPUT, INPUT_MOUSE, LPINPUT, MOUSEEVENTF_MOVE, MOUSEINPUT,
|
||||
MOUSEEVENTF_LEFTDOWN,
|
||||
MOUSEEVENTF_RIGHTDOWN,
|
||||
MOUSEEVENTF_MIDDLEDOWN,
|
||||
MOUSEEVENTF_LEFTUP,
|
||||
MOUSEEVENTF_RIGHTUP,
|
||||
MOUSEEVENTF_MIDDLEUP,
|
||||
MOUSEEVENTF_WHEEL,
|
||||
MOUSEEVENTF_HWHEEL, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_SCANCODE, KEYEVENTF_KEYUP,
|
||||
um::winuser::{
|
||||
INPUT, INPUT_KEYBOARD, INPUT_MOUSE, KEYBDINPUT, KEYEVENTF_KEYUP, KEYEVENTF_SCANCODE,
|
||||
LPINPUT, MOUSEEVENTF_HWHEEL, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP,
|
||||
MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN,
|
||||
MOUSEEVENTF_RIGHTUP, MOUSEEVENTF_WHEEL, MOUSEINPUT,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -18,15 +21,17 @@ use crate::{
|
||||
event::Event,
|
||||
};
|
||||
|
||||
|
||||
pub struct WindowsConsumer {}
|
||||
|
||||
impl WindowsConsumer {
|
||||
pub fn new() -> Self { Self { } }
|
||||
pub fn new() -> Result<Self> {
|
||||
Ok(Self {})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventConsumer for WindowsConsumer {
|
||||
fn consume(&self, event: Event, _: ClientHandle) {
|
||||
async fn consume(&mut self, event: Event, _: ClientHandle) {
|
||||
match event {
|
||||
Event::Pointer(pointer_event) => match pointer_event {
|
||||
PointerEvent::Motion {
|
||||
@@ -36,21 +41,35 @@ impl EventConsumer for WindowsConsumer {
|
||||
} => {
|
||||
rel_mouse(relative_x as i32, relative_y as i32);
|
||||
}
|
||||
PointerEvent::Button { time:_, button, state } => { mouse_button(button, state)}
|
||||
PointerEvent::Axis { time:_, axis, value } => { scroll(axis, value) }
|
||||
PointerEvent::Button {
|
||||
time: _,
|
||||
button,
|
||||
state,
|
||||
} => mouse_button(button, state),
|
||||
PointerEvent::Axis {
|
||||
time: _,
|
||||
axis,
|
||||
value,
|
||||
} => scroll(axis, value),
|
||||
PointerEvent::Frame {} => {}
|
||||
},
|
||||
Event::Keyboard(keyboard_event) => match keyboard_event {
|
||||
KeyboardEvent::Key { time:_, key, state } => { key_event(key, state) }
|
||||
KeyboardEvent::Key {
|
||||
time: _,
|
||||
key,
|
||||
state,
|
||||
} => key_event(key, state),
|
||||
KeyboardEvent::Modifiers { .. } => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn notify(&mut self, _: ClientEvent) {
|
||||
async fn notify(&mut self, _: ClientEvent) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
async fn destroy(&mut self) {}
|
||||
}
|
||||
|
||||
fn send_mouse_input(mi: MOUSEINPUT) {
|
||||
@@ -60,8 +79,8 @@ fn send_mouse_input(mi: MOUSEINPUT) {
|
||||
u: std::mem::transmute(mi),
|
||||
};
|
||||
|
||||
winapi::um::winuser::SendInput(
|
||||
1 as u32,
|
||||
SendInput(
|
||||
1_u32,
|
||||
&mut input as LPINPUT,
|
||||
std::mem::size_of::<INPUT>() as i32,
|
||||
);
|
||||
@@ -86,18 +105,19 @@ fn mouse_button(button: u32, state: u32) {
|
||||
0x110 => MOUSEEVENTF_LEFTUP,
|
||||
0x111 => MOUSEEVENTF_RIGHTUP,
|
||||
0x112 => MOUSEEVENTF_MIDDLEUP,
|
||||
_ => return
|
||||
}
|
||||
_ => return,
|
||||
},
|
||||
1 => match button {
|
||||
0x110 => MOUSEEVENTF_LEFTDOWN,
|
||||
0x111 => MOUSEEVENTF_RIGHTDOWN,
|
||||
0x112 => MOUSEEVENTF_MIDDLEDOWN,
|
||||
_ => return
|
||||
}
|
||||
_ => return
|
||||
_ => return,
|
||||
},
|
||||
_ => return,
|
||||
};
|
||||
let mi = MOUSEINPUT {
|
||||
dx: 0, dy: 0, // no movement
|
||||
dx: 0,
|
||||
dy: 0, // no movement
|
||||
mouseData: 0,
|
||||
dwFlags: dw_flags,
|
||||
time: 0,
|
||||
@@ -110,10 +130,11 @@ fn scroll(axis: u8, value: f64) {
|
||||
let event_type = match axis {
|
||||
0 => MOUSEEVENTF_WHEEL,
|
||||
1 => MOUSEEVENTF_HWHEEL,
|
||||
_ => return
|
||||
_ => return,
|
||||
};
|
||||
let mi = MOUSEINPUT {
|
||||
dx: 0, dy: 0,
|
||||
dx: 0,
|
||||
dy: 0,
|
||||
mouseData: (-value * 15.0) as i32 as u32,
|
||||
dwFlags: event_type,
|
||||
time: 0,
|
||||
@@ -123,14 +144,22 @@ fn scroll(axis: u8, value: f64) {
|
||||
}
|
||||
|
||||
fn key_event(key: u32, state: u8) {
|
||||
let scancode = match linux_keycode_to_windows_scancode(key) {
|
||||
Some(code) => code,
|
||||
None => return,
|
||||
};
|
||||
let extended = scancode > 0xff;
|
||||
let scancode = scancode & 0xff;
|
||||
let ki = KEYBDINPUT {
|
||||
wVk: 0,
|
||||
wScan: key as u16,
|
||||
dwFlags: KEYEVENTF_SCANCODE | match state {
|
||||
0 => KEYEVENTF_KEYUP,
|
||||
1 => 0u32,
|
||||
_ => return
|
||||
},
|
||||
wScan: scancode,
|
||||
dwFlags: KEYEVENTF_SCANCODE
|
||||
| if extended { KEYEVENTF_EXTENDEDKEY } else { 0 }
|
||||
| match state {
|
||||
0 => KEYEVENTF_KEYUP,
|
||||
1 => 0u32,
|
||||
_ => return,
|
||||
},
|
||||
time: 0,
|
||||
dwExtraInfo: 0,
|
||||
};
|
||||
@@ -144,6 +173,26 @@ fn send_keyboard_input(ki: KEYBDINPUT) {
|
||||
u: std::mem::zeroed(),
|
||||
};
|
||||
*input.u.ki_mut() = ki;
|
||||
winapi::um::winuser::SendInput(1 as u32, &mut input, std::mem::size_of::<INPUT>() as i32);
|
||||
SendInput(1_u32, &mut input, std::mem::size_of::<INPUT>() as i32);
|
||||
}
|
||||
}
|
||||
|
||||
fn linux_keycode_to_windows_scancode(linux_keycode: u32) -> Option<u16> {
|
||||
let linux_scancode = match scancode::Linux::try_from(linux_keycode) {
|
||||
Ok(s) => s,
|
||||
Err(_) => {
|
||||
log::warn!("unknown keycode: {linux_keycode}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
log::trace!("linux code: {linux_scancode:?}");
|
||||
let windows_scancode = match scancode::Windows::try_from(linux_scancode) {
|
||||
Ok(s) => s,
|
||||
Err(_) => {
|
||||
log::warn!("failed to translate linux code into windows scancode: {linux_scancode:?}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
log::trace!("windows code: {windows_scancode:?}");
|
||||
Some(windows_scancode as u16)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use wayland_client::WEnum;
|
||||
use crate::client::{ClientHandle, ClientEvent};
|
||||
use crate::client::{ClientEvent, ClientHandle};
|
||||
use crate::consumer::EventConsumer;
|
||||
use async_trait::async_trait;
|
||||
use std::collections::HashMap;
|
||||
use std::os::fd::OwnedFd;
|
||||
use std::os::unix::prelude::AsRawFd;
|
||||
use std::io;
|
||||
use std::os::fd::{AsFd, OwnedFd};
|
||||
use wayland_client::backend::WaylandError;
|
||||
use wayland_client::WEnum;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use wayland_client::globals::BindError;
|
||||
use wayland_client::protocol::wl_pointer::{Axis, ButtonState};
|
||||
use anyhow::{anyhow, Result};
|
||||
use wayland_client::protocol::wl_keyboard::{self, WlKeyboard};
|
||||
use wayland_client::protocol::wl_pointer::{Axis, ButtonState};
|
||||
use wayland_client::protocol::wl_seat::WlSeat;
|
||||
use wayland_protocols_wlr::virtual_pointer::v1::client::{
|
||||
zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1 as VpManager,
|
||||
@@ -20,8 +21,6 @@ use wayland_protocols_misc::zwp_virtual_keyboard_v1::client::{
|
||||
zwp_virtual_keyboard_v1::ZwpVirtualKeyboardV1 as Vk,
|
||||
};
|
||||
|
||||
use wayland_protocols_plasma::fake_input::client::org_kde_kwin_fake_input::OrgKdeKwinFakeInput;
|
||||
|
||||
use wayland_client::{
|
||||
delegate_noop,
|
||||
globals::{registry_queue_init, GlobalListContents},
|
||||
@@ -31,29 +30,26 @@ use wayland_client::{
|
||||
|
||||
use crate::event::{Event, KeyboardEvent, PointerEvent};
|
||||
|
||||
enum VirtualInputManager {
|
||||
Wlroots { vpm: VpManager, vkm: VkManager },
|
||||
Kde { fake_input: OrgKdeKwinFakeInput },
|
||||
}
|
||||
|
||||
struct State {
|
||||
keymap: Option<(u32, OwnedFd, u32)>,
|
||||
input_for_client: HashMap<ClientHandle, VirtualInput>,
|
||||
seat: wl_seat::WlSeat,
|
||||
virtual_input_manager: VirtualInputManager,
|
||||
qh: QueueHandle<Self>,
|
||||
vpm: VpManager,
|
||||
vkm: VkManager,
|
||||
}
|
||||
|
||||
// App State, implements Dispatch event handlers
|
||||
pub(crate) struct WlrootsConsumer {
|
||||
last_flush_failed: bool,
|
||||
state: State,
|
||||
queue: EventQueue<State>,
|
||||
}
|
||||
|
||||
impl WlrootsConsumer {
|
||||
pub fn new() -> Result<Self> {
|
||||
let conn = Connection::connect_to_env().unwrap();
|
||||
let (globals, queue) = registry_queue_init::<State>(&conn).unwrap();
|
||||
let conn = Connection::connect_to_env()?;
|
||||
let (globals, queue) = registry_queue_init::<State>(&conn)?;
|
||||
let qh = queue.handle();
|
||||
|
||||
let seat: wl_seat::WlSeat = match globals.bind(&qh, 7..=8, ()) {
|
||||
@@ -61,45 +57,28 @@ impl WlrootsConsumer {
|
||||
Err(_) => return Err(anyhow!("wl_seat >= v7 not supported")),
|
||||
};
|
||||
|
||||
let vpm: Result<VpManager, BindError> = globals.bind(&qh, 1..=1, ());
|
||||
let vkm: Result<VkManager, BindError> = globals.bind(&qh, 1..=1, ());
|
||||
let fake_input: Result<OrgKdeKwinFakeInput, BindError> = globals.bind(&qh, 4..=4, ());
|
||||
|
||||
let virtual_input_manager = match (vpm, vkm, fake_input) {
|
||||
(Ok(vpm), Ok(vkm), _) => VirtualInputManager::Wlroots { vpm, vkm },
|
||||
(_, _, Ok(fake_input)) => {
|
||||
fake_input.authenticate(
|
||||
"lan-mouse".into(),
|
||||
"Allow remote clients to control this device".into(),
|
||||
);
|
||||
VirtualInputManager::Kde { fake_input }
|
||||
}
|
||||
(Err(e1), Err(e2), Err(e3)) => {
|
||||
log::warn!("zwlr_virtual_pointer_v1: {e1}");
|
||||
log::warn!("zwp_virtual_keyboard_v1: {e2}");
|
||||
log::warn!("org_kde_kwin_fake_input: {e3}");
|
||||
log::error!("neither wlroots nor kde input emulation protocol supported!");
|
||||
return Err(anyhow!("could not create event consumer"));
|
||||
}
|
||||
_ => {
|
||||
panic!()
|
||||
}
|
||||
};
|
||||
let vpm: VpManager = globals.bind(&qh, 1..=1, ())?;
|
||||
let vkm: VkManager = globals.bind(&qh, 1..=1, ())?;
|
||||
|
||||
let input_for_client: HashMap<ClientHandle, VirtualInput> = HashMap::new();
|
||||
|
||||
let mut consumer = WlrootsConsumer {
|
||||
last_flush_failed: false,
|
||||
state: State {
|
||||
keymap: None,
|
||||
input_for_client,
|
||||
seat,
|
||||
virtual_input_manager,
|
||||
vpm,
|
||||
vkm,
|
||||
qh,
|
||||
},
|
||||
queue,
|
||||
};
|
||||
while consumer.state.keymap.is_none() {
|
||||
consumer.queue.blocking_dispatch(&mut consumer.state).unwrap();
|
||||
consumer
|
||||
.queue
|
||||
.blocking_dispatch(&mut consumer.state)
|
||||
.unwrap();
|
||||
}
|
||||
// let fd = unsafe { &File::from_raw_fd(consumer.state.keymap.unwrap().1.as_raw_fd()) };
|
||||
// let mmap = unsafe { MmapOptions::new().map_copy(fd).unwrap() };
|
||||
@@ -110,43 +89,61 @@ impl WlrootsConsumer {
|
||||
|
||||
impl State {
|
||||
fn add_client(&mut self, client: ClientHandle) {
|
||||
// create virtual input devices
|
||||
match &self.virtual_input_manager {
|
||||
VirtualInputManager::Wlroots { vpm, vkm } => {
|
||||
let pointer: Vp = vpm.create_virtual_pointer(None, &self.qh, ());
|
||||
let keyboard: Vk = vkm.create_virtual_keyboard(&self.seat, &self.qh, ());
|
||||
let pointer: Vp = self.vpm.create_virtual_pointer(None, &self.qh, ());
|
||||
let keyboard: Vk = self.vkm.create_virtual_keyboard(&self.seat, &self.qh, ());
|
||||
|
||||
// TODO: use server side keymap
|
||||
if let Some((format, fd, size)) = self.keymap.as_ref() {
|
||||
keyboard.keymap(*format, fd.as_raw_fd(), *size);
|
||||
} else {
|
||||
panic!("no keymap");
|
||||
}
|
||||
|
||||
let vinput = VirtualInput::Wlroots { pointer, keyboard };
|
||||
|
||||
self.input_for_client.insert(client, vinput);
|
||||
}
|
||||
VirtualInputManager::Kde { fake_input } => {
|
||||
let fake_input = fake_input.clone();
|
||||
let vinput = VirtualInput::Kde { fake_input };
|
||||
self.input_for_client.insert(client, vinput);
|
||||
}
|
||||
// TODO: use server side keymap
|
||||
if let Some((format, fd, size)) = self.keymap.as_ref() {
|
||||
keyboard.keymap(*format, fd.as_fd(), *size);
|
||||
} else {
|
||||
panic!("no keymap");
|
||||
}
|
||||
|
||||
let vinput = VirtualInput { pointer, keyboard };
|
||||
|
||||
self.input_for_client.insert(client, vinput);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventConsumer for WlrootsConsumer {
|
||||
fn consume(&self, event: Event, client_handle: ClientHandle) {
|
||||
async fn consume(&mut self, event: Event, client_handle: ClientHandle) {
|
||||
if let Some(virtual_input) = self.state.input_for_client.get(&client_handle) {
|
||||
if self.last_flush_failed {
|
||||
if let Err(WaylandError::Io(e)) = self.queue.flush() {
|
||||
if e.kind() == io::ErrorKind::WouldBlock {
|
||||
/*
|
||||
* outgoing buffer is full - sending more events
|
||||
* will overwhelm the output buffer and leave the
|
||||
* wayland connection in a broken state
|
||||
*/
|
||||
log::warn!(
|
||||
"can't keep up, discarding event: ({client_handle}) - {event:?}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
virtual_input.consume_event(event).unwrap();
|
||||
if let Err(e) = self.queue.flush() {
|
||||
log::error!("{}", e);
|
||||
match self.queue.flush() {
|
||||
Err(WaylandError::Io(e)) if e.kind() == io::ErrorKind::WouldBlock => {
|
||||
self.last_flush_failed = true;
|
||||
log::warn!("can't keep up, retrying ...");
|
||||
}
|
||||
Err(WaylandError::Io(e)) => {
|
||||
log::error!("{e}")
|
||||
}
|
||||
Err(WaylandError::Protocol(e)) => {
|
||||
panic!("wayland protocol violation: {e}")
|
||||
}
|
||||
Ok(()) => {
|
||||
self.last_flush_failed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn notify(&mut self, client_event: ClientEvent) {
|
||||
async fn notify(&mut self, client_event: ClientEvent) {
|
||||
if let ClientEvent::Create(client, _) = client_event {
|
||||
self.state.add_client(client);
|
||||
if let Err(e) = self.queue.flush() {
|
||||
@@ -154,104 +151,57 @@ impl EventConsumer for WlrootsConsumer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn destroy(&mut self) {}
|
||||
}
|
||||
|
||||
|
||||
enum VirtualInput {
|
||||
Wlroots { pointer: Vp, keyboard: Vk },
|
||||
Kde { fake_input: OrgKdeKwinFakeInput },
|
||||
struct VirtualInput {
|
||||
pointer: Vp,
|
||||
keyboard: Vk,
|
||||
}
|
||||
|
||||
impl VirtualInput {
|
||||
fn consume_event(&self, event: Event) -> Result<(),()> {
|
||||
fn consume_event(&self, event: Event) -> Result<(), ()> {
|
||||
match event {
|
||||
Event::Pointer(e) => match e {
|
||||
PointerEvent::Motion {
|
||||
time,
|
||||
relative_x,
|
||||
relative_y,
|
||||
} => match self {
|
||||
VirtualInput::Wlroots {
|
||||
pointer,
|
||||
keyboard: _,
|
||||
Event::Pointer(e) => {
|
||||
match e {
|
||||
PointerEvent::Motion {
|
||||
time,
|
||||
relative_x,
|
||||
relative_y,
|
||||
} => self.pointer.motion(time, relative_x, relative_y),
|
||||
PointerEvent::Button {
|
||||
time,
|
||||
button,
|
||||
state,
|
||||
} => {
|
||||
pointer.motion(time, relative_x, relative_y);
|
||||
let state: ButtonState = state.try_into()?;
|
||||
self.pointer.button(time, button, state);
|
||||
}
|
||||
VirtualInput::Kde { fake_input } => {
|
||||
fake_input.pointer_motion(relative_y, relative_y);
|
||||
}
|
||||
},
|
||||
PointerEvent::Button {
|
||||
time,
|
||||
button,
|
||||
state,
|
||||
} => {
|
||||
let state: ButtonState = state.try_into()?;
|
||||
match self {
|
||||
VirtualInput::Wlroots {
|
||||
pointer,
|
||||
keyboard: _,
|
||||
} => {
|
||||
pointer.button(time, button, state);
|
||||
}
|
||||
VirtualInput::Kde { fake_input } => {
|
||||
fake_input.button(button, state as u32);
|
||||
}
|
||||
PointerEvent::Axis { time, axis, value } => {
|
||||
let axis: Axis = (axis as u32).try_into()?;
|
||||
self.pointer.axis(time, axis, value);
|
||||
self.pointer.frame();
|
||||
}
|
||||
PointerEvent::Frame {} => self.pointer.frame(),
|
||||
}
|
||||
PointerEvent::Axis { time, axis, value } => {
|
||||
let axis: Axis = (axis as u32).try_into()?;
|
||||
match self {
|
||||
VirtualInput::Wlroots {
|
||||
pointer,
|
||||
keyboard: _,
|
||||
} => {
|
||||
pointer.axis(time, axis, value);
|
||||
pointer.frame();
|
||||
}
|
||||
VirtualInput::Kde { fake_input } => {
|
||||
fake_input.axis(axis as u32, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
PointerEvent::Frame {} => match self {
|
||||
VirtualInput::Wlroots {
|
||||
pointer,
|
||||
keyboard: _,
|
||||
} => {
|
||||
pointer.frame();
|
||||
}
|
||||
VirtualInput::Kde { fake_input: _ } => {}
|
||||
},
|
||||
},
|
||||
self.pointer.frame();
|
||||
}
|
||||
Event::Keyboard(e) => match e {
|
||||
KeyboardEvent::Key { time, key, state } => match self {
|
||||
VirtualInput::Wlroots {
|
||||
pointer: _,
|
||||
keyboard,
|
||||
} => {
|
||||
keyboard.key(time, key, state as u32);
|
||||
}
|
||||
VirtualInput::Kde { fake_input } => {
|
||||
fake_input.keyboard_key(key, state as u32);
|
||||
}
|
||||
},
|
||||
KeyboardEvent::Key { time, key, state } => {
|
||||
self.keyboard.key(time, key, state as u32);
|
||||
}
|
||||
KeyboardEvent::Modifiers {
|
||||
mods_depressed,
|
||||
mods_latched,
|
||||
mods_locked,
|
||||
group,
|
||||
} => match self {
|
||||
VirtualInput::Wlroots {
|
||||
pointer: _,
|
||||
keyboard,
|
||||
} => {
|
||||
keyboard.modifiers(mods_depressed, mods_latched, mods_locked, group);
|
||||
}
|
||||
VirtualInput::Kde { fake_input: _ } => {}
|
||||
},
|
||||
} => {
|
||||
self.keyboard
|
||||
.modifiers(mods_depressed, mods_latched, mods_locked, group);
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -261,7 +211,6 @@ delegate_noop!(State: Vp);
|
||||
delegate_noop!(State: Vk);
|
||||
delegate_noop!(State: VpManager);
|
||||
delegate_noop!(State: VkManager);
|
||||
delegate_noop!(State: OrgKdeKwinFakeInput);
|
||||
|
||||
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for State {
|
||||
fn event(
|
||||
@@ -284,11 +233,8 @@ impl Dispatch<WlKeyboard, ()> for State {
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
wl_keyboard::Event::Keymap { format, fd, size } => {
|
||||
state.keymap = Some((u32::from(format), fd, size));
|
||||
}
|
||||
_ => {},
|
||||
if let wl_keyboard::Event::Keymap { format, fd, size } = event {
|
||||
state.keymap = Some((u32::from(format), fd, size));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +1,149 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use std::ptr;
|
||||
use x11::{xlib, xtest};
|
||||
use x11::{
|
||||
xlib::{self, XCloseDisplay},
|
||||
xtest,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
client::ClientHandle,
|
||||
event::Event, consumer::EventConsumer,
|
||||
consumer::EventConsumer,
|
||||
event::{
|
||||
Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT,
|
||||
},
|
||||
};
|
||||
|
||||
pub struct X11Consumer {
|
||||
display: *mut xlib::Display,
|
||||
}
|
||||
|
||||
unsafe impl Send for X11Consumer {}
|
||||
|
||||
impl X11Consumer {
|
||||
pub fn new() -> Self {
|
||||
pub fn new() -> Result<Self> {
|
||||
let display = unsafe {
|
||||
match xlib::XOpenDisplay(ptr::null()) {
|
||||
d if d == ptr::null::<xlib::Display>() as *mut xlib::Display => None,
|
||||
display => Some(display),
|
||||
d if d == ptr::null::<xlib::Display>() as *mut xlib::Display => {
|
||||
Err(anyhow!("could not open display"))
|
||||
}
|
||||
display => Ok(display),
|
||||
}
|
||||
};
|
||||
let display = display.expect("could not open display");
|
||||
Self { display }
|
||||
}?;
|
||||
Ok(Self { display })
|
||||
}
|
||||
|
||||
fn relative_motion(&self, dx: i32, dy: i32) {
|
||||
unsafe {
|
||||
xtest::XTestFakeRelativeMotionEvent(self.display, dx, dy, 0, 0);
|
||||
xlib::XFlush(self.display);
|
||||
}
|
||||
}
|
||||
|
||||
fn emulate_mouse_button(&self, button: u32, state: u32) {
|
||||
unsafe {
|
||||
let x11_button = match button {
|
||||
BTN_RIGHT => 3,
|
||||
BTN_MIDDLE => 2,
|
||||
BTN_BACK => 8,
|
||||
BTN_FORWARD => 9,
|
||||
BTN_LEFT => 1,
|
||||
_ => 1,
|
||||
};
|
||||
xtest::XTestFakeButtonEvent(self.display, x11_button, state as i32, 0);
|
||||
};
|
||||
}
|
||||
|
||||
const SCROLL_UP: u32 = 4;
|
||||
const SCROLL_DOWN: u32 = 5;
|
||||
const SCROLL_LEFT: u32 = 6;
|
||||
const SCROLL_RIGHT: u32 = 7;
|
||||
|
||||
fn emulate_scroll(&self, axis: u8, value: f64) {
|
||||
let direction = match axis {
|
||||
1 => {
|
||||
if value < 0.0 {
|
||||
Self::SCROLL_LEFT
|
||||
} else {
|
||||
Self::SCROLL_RIGHT
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if value < 0.0 {
|
||||
Self::SCROLL_UP
|
||||
} else {
|
||||
Self::SCROLL_DOWN
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
unsafe {
|
||||
xtest::XTestFakeButtonEvent(self.display, direction, 1, 0);
|
||||
xtest::XTestFakeButtonEvent(self.display, direction, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn emulate_key(&self, key: u32, state: u8) {
|
||||
let key = key + 8; // xorg keycodes are shifted by 8
|
||||
unsafe {
|
||||
xtest::XTestFakeKeyEvent(self.display, key, state as i32, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for X11Consumer {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
XCloseDisplay(self.display);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventConsumer for X11Consumer {
|
||||
fn consume(&self, event: Event, _: ClientHandle) {
|
||||
async fn consume(&mut self, event: Event, _: ClientHandle) {
|
||||
match event {
|
||||
Event::Pointer(pointer_event) => match pointer_event {
|
||||
crate::event::PointerEvent::Motion {
|
||||
PointerEvent::Motion {
|
||||
time: _,
|
||||
relative_x,
|
||||
relative_y,
|
||||
} => {
|
||||
self.relative_motion(relative_x as i32, relative_y as i32);
|
||||
}
|
||||
crate::event::PointerEvent::Button { .. } => {}
|
||||
crate::event::PointerEvent::Axis { .. } => {}
|
||||
crate::event::PointerEvent::Frame {} => {}
|
||||
PointerEvent::Button {
|
||||
time: _,
|
||||
button,
|
||||
state,
|
||||
} => {
|
||||
self.emulate_mouse_button(button, state);
|
||||
}
|
||||
PointerEvent::Axis {
|
||||
time: _,
|
||||
axis,
|
||||
value,
|
||||
} => {
|
||||
self.emulate_scroll(axis, value);
|
||||
}
|
||||
PointerEvent::Frame {} => {}
|
||||
},
|
||||
Event::Keyboard(_) => {}
|
||||
Event::Keyboard(KeyboardEvent::Key {
|
||||
time: _,
|
||||
key,
|
||||
state,
|
||||
}) => {
|
||||
self.emulate_key(key, state);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
unsafe {
|
||||
xlib::XFlush(self.display);
|
||||
}
|
||||
}
|
||||
|
||||
fn notify(&mut self, _: crate::client::ClientEvent) {
|
||||
async fn notify(&mut self, _: crate::client::ClientEvent) {
|
||||
// for our purposes it does not matter what client sent the event
|
||||
}
|
||||
}
|
||||
|
||||
async fn destroy(&mut self) {}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,139 @@
|
||||
use crate::consumer::EventConsumer;
|
||||
use anyhow::Result;
|
||||
use ashpd::{
|
||||
desktop::{
|
||||
remote_desktop::{Axis, DeviceType, KeyState, RemoteDesktop},
|
||||
Session,
|
||||
},
|
||||
WindowIdentifier,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
|
||||
pub struct DesktopPortalConsumer {}
|
||||
use crate::{
|
||||
client::ClientEvent,
|
||||
consumer::EventConsumer,
|
||||
event::{
|
||||
Event::{Keyboard, Pointer},
|
||||
KeyboardEvent, PointerEvent,
|
||||
},
|
||||
};
|
||||
|
||||
impl DesktopPortalConsumer {
|
||||
pub fn new() -> Self { Self { } }
|
||||
pub struct DesktopPortalConsumer<'a> {
|
||||
proxy: RemoteDesktop<'a>,
|
||||
session: Session<'a>,
|
||||
}
|
||||
|
||||
impl EventConsumer for DesktopPortalConsumer {
|
||||
fn consume(&self, _: crate::event::Event, _: crate::client::ClientHandle) {
|
||||
log::error!("xdg_desktop_portal backend not yet implemented!");
|
||||
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()?;
|
||||
log::debug!("started session");
|
||||
|
||||
Ok(Self { proxy, session })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'a> EventConsumer for DesktopPortalConsumer<'a> {
|
||||
async fn consume(&mut self, event: crate::event::Event, _client: crate::client::ClientHandle) {
|
||||
match event {
|
||||
Pointer(p) => {
|
||||
match p {
|
||||
PointerEvent::Motion {
|
||||
time: _,
|
||||
relative_x,
|
||||
relative_y,
|
||||
} => {
|
||||
if let Err(e) = self
|
||||
.proxy
|
||||
.notify_pointer_motion(&self.session, relative_x, relative_y)
|
||||
.await
|
||||
{
|
||||
log::warn!("{e}");
|
||||
}
|
||||
}
|
||||
PointerEvent::Button {
|
||||
time: _,
|
||||
button,
|
||||
state,
|
||||
} => {
|
||||
let state = match state {
|
||||
0 => KeyState::Released,
|
||||
_ => KeyState::Pressed,
|
||||
};
|
||||
if let Err(e) = self
|
||||
.proxy
|
||||
.notify_pointer_button(&self.session, button as i32, state)
|
||||
.await
|
||||
{
|
||||
log::warn!("{e}");
|
||||
}
|
||||
}
|
||||
PointerEvent::Axis {
|
||||
time: _,
|
||||
axis,
|
||||
value,
|
||||
} => {
|
||||
let axis = match axis {
|
||||
0 => Axis::Vertical,
|
||||
_ => Axis::Horizontal,
|
||||
};
|
||||
// TODO smooth scrolling
|
||||
if let Err(e) = self
|
||||
.proxy
|
||||
.notify_pointer_axis_discrete(&self.session, axis, value as i32)
|
||||
.await
|
||||
{
|
||||
log::warn!("{e}");
|
||||
}
|
||||
}
|
||||
PointerEvent::Frame {} => {}
|
||||
}
|
||||
}
|
||||
Keyboard(k) => {
|
||||
match k {
|
||||
KeyboardEvent::Key {
|
||||
time: _,
|
||||
key,
|
||||
state,
|
||||
} => {
|
||||
let state = match state {
|
||||
0 => KeyState::Released,
|
||||
_ => KeyState::Pressed,
|
||||
};
|
||||
if let Err(e) = self
|
||||
.proxy
|
||||
.notify_keyboard_keycode(&self.session, key as i32, state)
|
||||
.await
|
||||
{
|
||||
log::warn!("{e}");
|
||||
}
|
||||
}
|
||||
KeyboardEvent::Modifiers { .. } => {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn notify(&mut self, _: crate::client::ClientEvent) {}
|
||||
async fn notify(&mut self, _client: ClientEvent) {}
|
||||
|
||||
async fn destroy(&mut self) {
|
||||
log::debug!("closing remote desktop session");
|
||||
if let Err(e) = self.session.close().await {
|
||||
log::error!("failed to close remote desktop session: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
#[cfg(all(unix, feature = "wayland"))]
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
pub mod libei;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod macos;
|
||||
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
pub mod wayland;
|
||||
|
||||
#[cfg(windows)]
|
||||
pub mod windows;
|
||||
#[cfg(all(unix, feature = "x11"))]
|
||||
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
pub mod x11;
|
||||
|
||||
/// fallback event producer
|
||||
pub mod dummy;
|
||||
|
||||
38
src/backend/producer/dummy.rs
Normal file
38
src/backend/producer/dummy.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use std::io;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use futures_core::Stream;
|
||||
|
||||
use crate::event::Event;
|
||||
use crate::producer::EventProducer;
|
||||
|
||||
use crate::client::{ClientEvent, ClientHandle};
|
||||
|
||||
pub struct DummyProducer {}
|
||||
|
||||
impl DummyProducer {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DummyProducer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventProducer for DummyProducer {
|
||||
fn notify(&mut self, _: ClientEvent) {}
|
||||
|
||||
fn release(&mut self) {}
|
||||
}
|
||||
|
||||
impl Stream for DummyProducer {
|
||||
type Item = io::Result<(ClientHandle, Event)>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
31
src/backend/producer/libei.rs
Normal file
31
src/backend/producer/libei.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::{io, task::Poll};
|
||||
|
||||
use futures_core::Stream;
|
||||
|
||||
use crate::{client::ClientHandle, event::Event, producer::EventProducer};
|
||||
|
||||
pub struct LibeiProducer {}
|
||||
|
||||
impl LibeiProducer {
|
||||
pub fn new() -> Result<Self> {
|
||||
Err(anyhow!("not implemented"))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventProducer for LibeiProducer {
|
||||
fn notify(&mut self, _event: crate::client::ClientEvent) {}
|
||||
|
||||
fn release(&mut self) {}
|
||||
}
|
||||
|
||||
impl Stream for LibeiProducer {
|
||||
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
|
||||
}
|
||||
}
|
||||
29
src/backend/producer/macos.rs
Normal file
29
src/backend/producer/macos.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use crate::client::{ClientEvent, ClientHandle};
|
||||
use crate::event::Event;
|
||||
use crate::producer::EventProducer;
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures_core::Stream;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{io, pin::Pin};
|
||||
|
||||
pub struct MacOSProducer;
|
||||
|
||||
impl MacOSProducer {
|
||||
pub fn new() -> Result<Self> {
|
||||
Err(anyhow!("not yet implemented"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for MacOSProducer {
|
||||
type Item = io::Result<(ClientHandle, Event)>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
|
||||
impl EventProducer for MacOSProducer {
|
||||
fn notify(&mut self, _event: ClientEvent) {}
|
||||
|
||||
fn release(&mut self) {}
|
||||
}
|
||||
@@ -1,9 +1,20 @@
|
||||
use crate::{client::{ClientHandle, Position, ClientEvent}, producer::EventProducer};
|
||||
use mio::{event::Source, unix::SourceFd};
|
||||
use crate::{
|
||||
client::{ClientEvent, ClientHandle, Position},
|
||||
producer::EventProducer,
|
||||
};
|
||||
|
||||
use std::{os::fd::RawFd, vec::Drain, io::ErrorKind};
|
||||
use memmap::MmapOptions;
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures_core::Stream;
|
||||
use memmap::MmapOptions;
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
env,
|
||||
io::{self, ErrorKind},
|
||||
os::fd::{AsFd, OwnedFd, RawFd},
|
||||
pin::Pin,
|
||||
task::{ready, Context, Poll},
|
||||
};
|
||||
use tokio::io::unix::AsyncFd;
|
||||
|
||||
use std::{
|
||||
fs::File,
|
||||
@@ -12,18 +23,24 @@ use std::{
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use wayland_protocols::wp::{
|
||||
keyboard_shortcuts_inhibit::zv1::client::{
|
||||
zwp_keyboard_shortcuts_inhibit_manager_v1::ZwpKeyboardShortcutsInhibitManagerV1,
|
||||
zwp_keyboard_shortcuts_inhibitor_v1::ZwpKeyboardShortcutsInhibitorV1,
|
||||
use wayland_protocols::{
|
||||
wp::{
|
||||
keyboard_shortcuts_inhibit::zv1::client::{
|
||||
zwp_keyboard_shortcuts_inhibit_manager_v1::ZwpKeyboardShortcutsInhibitManagerV1,
|
||||
zwp_keyboard_shortcuts_inhibitor_v1::ZwpKeyboardShortcutsInhibitorV1,
|
||||
},
|
||||
pointer_constraints::zv1::client::{
|
||||
zwp_locked_pointer_v1::ZwpLockedPointerV1,
|
||||
zwp_pointer_constraints_v1::{Lifetime, ZwpPointerConstraintsV1},
|
||||
},
|
||||
relative_pointer::zv1::client::{
|
||||
zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1,
|
||||
zwp_relative_pointer_v1::{self, ZwpRelativePointerV1},
|
||||
},
|
||||
},
|
||||
pointer_constraints::zv1::client::{
|
||||
zwp_locked_pointer_v1::ZwpLockedPointerV1,
|
||||
zwp_pointer_constraints_v1::{Lifetime, ZwpPointerConstraintsV1},
|
||||
},
|
||||
relative_pointer::zv1::client::{
|
||||
zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1,
|
||||
zwp_relative_pointer_v1::{self, ZwpRelativePointerV1},
|
||||
xdg::xdg_output::zv1::client::{
|
||||
zxdg_output_manager_v1::ZxdgOutputManagerV1,
|
||||
zxdg_output_v1::{self, ZxdgOutputV1},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -33,14 +50,15 @@ use wayland_protocols_wlr::layer_shell::v1::client::{
|
||||
};
|
||||
|
||||
use wayland_client::{
|
||||
backend::{WaylandError, ReadEventsGuard},
|
||||
backend::{ReadEventsGuard, WaylandError},
|
||||
delegate_noop,
|
||||
globals::{registry_queue_init, GlobalListContents},
|
||||
protocol::{
|
||||
wl_buffer, wl_compositor, wl_keyboard, wl_pointer, wl_region, wl_registry, wl_seat, wl_shm,
|
||||
wl_shm_pool, wl_surface,
|
||||
wl_buffer, wl_compositor, wl_keyboard,
|
||||
wl_output::{self, WlOutput},
|
||||
wl_pointer, wl_region, wl_registry, wl_seat, wl_shm, wl_shm_pool, wl_surface,
|
||||
},
|
||||
Connection, Dispatch, DispatchError, QueueHandle, WEnum, EventQueue,
|
||||
Connection, Dispatch, DispatchError, EventQueue, QueueHandle, WEnum,
|
||||
};
|
||||
|
||||
use tempfile;
|
||||
@@ -55,6 +73,25 @@ struct Globals {
|
||||
seat: wl_seat::WlSeat,
|
||||
shm: wl_shm::WlShm,
|
||||
layer_shell: ZwlrLayerShellV1,
|
||||
outputs: Vec<wl_output::WlOutput>,
|
||||
xdg_output_manager: ZxdgOutputManagerV1,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct OutputInfo {
|
||||
name: String,
|
||||
position: (i32, i32),
|
||||
size: (i32, i32),
|
||||
}
|
||||
|
||||
impl OutputInfo {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
name: "".to_string(),
|
||||
position: (0, 0),
|
||||
size: (0, 0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct State {
|
||||
@@ -64,17 +101,26 @@ struct State {
|
||||
client_for_window: Vec<(Rc<Window>, ClientHandle)>,
|
||||
focused: Option<(Rc<Window>, ClientHandle)>,
|
||||
g: Globals,
|
||||
wayland_fd: RawFd,
|
||||
wayland_fd: OwnedFd,
|
||||
read_guard: Option<ReadEventsGuard>,
|
||||
qh: QueueHandle<Self>,
|
||||
pending_events: Vec<(ClientHandle, Event)>,
|
||||
pending_events: VecDeque<(ClientHandle, Event)>,
|
||||
output_info: Vec<(WlOutput, OutputInfo)>,
|
||||
}
|
||||
|
||||
pub struct WaylandEventProducer {
|
||||
struct Inner {
|
||||
state: State,
|
||||
queue: EventQueue<State>,
|
||||
}
|
||||
|
||||
impl AsRawFd for Inner {
|
||||
fn as_raw_fd(&self) -> RawFd {
|
||||
self.state.wayland_fd.as_raw_fd()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WaylandEventProducer(AsyncFd<Inner>);
|
||||
|
||||
struct Window {
|
||||
buffer: wl_buffer::WlBuffer,
|
||||
surface: wl_surface::WlSurface,
|
||||
@@ -82,13 +128,24 @@ struct Window {
|
||||
}
|
||||
|
||||
impl Window {
|
||||
fn new(g: &Globals, qh: &QueueHandle<State>, pos: Position) -> Window {
|
||||
let (width, height) = (1, 1440);
|
||||
fn new(
|
||||
state: &State,
|
||||
qh: &QueueHandle<State>,
|
||||
output: &WlOutput,
|
||||
pos: Position,
|
||||
size: (i32, i32),
|
||||
) -> Window {
|
||||
let g = &state.g;
|
||||
|
||||
let (width, height) = match pos {
|
||||
Position::Left | Position::Right => (1, size.1 as u32),
|
||||
Position::Top | Position::Bottom => (size.0 as u32, 1),
|
||||
};
|
||||
let mut file = tempfile::tempfile().unwrap();
|
||||
draw(&mut file, (width, height));
|
||||
let pool = g
|
||||
.shm
|
||||
.create_pool(file.as_raw_fd(), (width * height * 4) as i32, qh, ());
|
||||
.create_pool(file.as_fd(), (width * height * 4) as i32, qh, ());
|
||||
let buffer = pool.create_buffer(
|
||||
0,
|
||||
width as i32,
|
||||
@@ -102,8 +159,8 @@ impl Window {
|
||||
|
||||
let layer_surface = g.layer_shell.get_layer_surface(
|
||||
&surface,
|
||||
None,
|
||||
Layer::Top,
|
||||
Some(output),
|
||||
Layer::Overlay,
|
||||
"LAN Mouse Sharing".into(),
|
||||
qh,
|
||||
(),
|
||||
@@ -116,8 +173,8 @@ impl Window {
|
||||
};
|
||||
|
||||
layer_surface.set_anchor(anchor);
|
||||
layer_surface.set_size(1, 1440);
|
||||
layer_surface.set_exclusive_zone(0);
|
||||
layer_surface.set_size(width, height);
|
||||
layer_surface.set_exclusive_zone(-1);
|
||||
layer_surface.set_margin(0, 0, 0, 0);
|
||||
surface.set_input_region(None);
|
||||
surface.commit();
|
||||
@@ -129,20 +186,79 @@ impl Window {
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Window {
|
||||
fn drop(&mut self) {
|
||||
log::debug!("destroying window!");
|
||||
self.layer_surface.destroy();
|
||||
self.surface.destroy();
|
||||
self.buffer.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
fn get_edges(outputs: &[(WlOutput, OutputInfo)], pos: Position) -> Vec<(WlOutput, i32)> {
|
||||
outputs
|
||||
.iter()
|
||||
.map(|(o, i)| {
|
||||
(
|
||||
o.clone(),
|
||||
match pos {
|
||||
Position::Left => i.position.0,
|
||||
Position::Right => i.position.0 + i.size.0,
|
||||
Position::Top => i.position.1,
|
||||
Position::Bottom => i.position.1 + i.size.1,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
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);
|
||||
let opposite_edges = get_edges(&state.output_info, pos.opposite());
|
||||
|
||||
// remove those edges that are at the same position
|
||||
// as an opposite edge of a different output
|
||||
let outputs: Vec<WlOutput> = edges
|
||||
.iter()
|
||||
.filter(|(_, edge)| !opposite_edges.iter().map(|(_, e)| *e).any(|e| &e == edge))
|
||||
.map(|(o, _)| o.clone())
|
||||
.collect();
|
||||
state
|
||||
.output_info
|
||||
.iter()
|
||||
.filter(|(o, _)| outputs.contains(o))
|
||||
.map(|(o, i)| (o.clone(), i.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn draw(f: &mut File, (width, height): (u32, u32)) {
|
||||
let mut buf = BufWriter::new(f);
|
||||
for _ in 0..height {
|
||||
for _ in 0..width {
|
||||
buf.write_all(&0x44FbF1C7u32.to_ne_bytes()).unwrap();
|
||||
if env::var("LM_DEBUG_LAYER_SHELL").ok().is_some() {
|
||||
// AARRGGBB
|
||||
buf.write_all(&0xff11d116u32.to_ne_bytes()).unwrap();
|
||||
} else {
|
||||
// AARRGGBB
|
||||
buf.write_all(&0x00000000u32.to_ne_bytes()).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WaylandEventProducer {
|
||||
pub fn new() -> Result<Self> {
|
||||
let conn = Connection::connect_to_env().expect("could not connect to wayland compositor");
|
||||
let (g, queue) =
|
||||
registry_queue_init::<State>(&conn).expect("failed to initialize wl_registry");
|
||||
let conn = match Connection::connect_to_env() {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(anyhow!("could not connect to wayland compositor: {e:?}")),
|
||||
};
|
||||
|
||||
let (g, mut queue) = match registry_queue_init::<State>(&conn) {
|
||||
Ok(q) => q,
|
||||
Err(e) => return Err(anyhow!("failed to initialize wl_registry: {e:?}")),
|
||||
};
|
||||
|
||||
let qh = queue.handle();
|
||||
|
||||
let compositor: wl_compositor::WlCompositor = match g.bind(&qh, 4..=5, ()) {
|
||||
@@ -150,6 +266,11 @@ impl WaylandEventProducer {
|
||||
Err(_) => return Err(anyhow!("wl_compositor >= v4 not supported")),
|
||||
};
|
||||
|
||||
let xdg_output_manager: ZxdgOutputManagerV1 = match g.bind(&qh, 1..=3, ()) {
|
||||
Ok(xdg_output_manager) => xdg_output_manager,
|
||||
Err(_) => return Err(anyhow!("xdg_output not supported!")),
|
||||
};
|
||||
|
||||
let shm: wl_shm::WlShm = match g.bind(&qh, 1..=1, ()) {
|
||||
Ok(wl_shm) => wl_shm,
|
||||
Err(_) => return Err(anyhow!("wl_shm v1 not supported")),
|
||||
@@ -175,10 +296,17 @@ impl WaylandEventProducer {
|
||||
Err(_) => return Err(anyhow!("zwp_relative_pointer_manager_v1 not supported")),
|
||||
};
|
||||
|
||||
let shortcut_inhibit_manager: ZwpKeyboardShortcutsInhibitManagerV1 = match g.bind(&qh, 1..=1, ()) {
|
||||
Ok(shortcut_inhibit_manager) => shortcut_inhibit_manager,
|
||||
Err(_) => return Err(anyhow!("zwp_keyboard_shortcuts_inhibit_manager_v1 not supported")),
|
||||
};
|
||||
let shortcut_inhibit_manager: ZwpKeyboardShortcutsInhibitManagerV1 =
|
||||
match g.bind(&qh, 1..=1, ()) {
|
||||
Ok(shortcut_inhibit_manager) => shortcut_inhibit_manager,
|
||||
Err(_) => {
|
||||
return Err(anyhow!(
|
||||
"zwp_keyboard_shortcuts_inhibit_manager_v1 not supported"
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let outputs = vec![];
|
||||
|
||||
let g = Globals {
|
||||
compositor,
|
||||
@@ -188,31 +316,70 @@ impl WaylandEventProducer {
|
||||
pointer_constraints,
|
||||
relative_pointer_manager,
|
||||
shortcut_inhibit_manager,
|
||||
outputs,
|
||||
xdg_output_manager,
|
||||
};
|
||||
|
||||
// flush outgoing events
|
||||
queue.flush()?;
|
||||
|
||||
// prepare reading wayland events
|
||||
let read_guard = queue.prepare_read()?;
|
||||
let wayland_fd = read_guard.connection_fd().as_raw_fd();
|
||||
let read_guard = Some(read_guard);
|
||||
let read_guard = queue.prepare_read().unwrap(); // there can not yet be events to dispatch
|
||||
let wayland_fd = read_guard.connection_fd().try_clone_to_owned().unwrap();
|
||||
std::mem::drop(read_guard);
|
||||
|
||||
Ok(WaylandEventProducer {
|
||||
queue,
|
||||
state: State {
|
||||
g,
|
||||
pointer_lock: None,
|
||||
rel_pointer: None,
|
||||
shortcut_inhibitor: None,
|
||||
client_for_window: Vec::new(),
|
||||
focused: None,
|
||||
qh,
|
||||
wayland_fd,
|
||||
read_guard,
|
||||
pending_events: vec![],
|
||||
let mut state = State {
|
||||
g,
|
||||
pointer_lock: None,
|
||||
rel_pointer: None,
|
||||
shortcut_inhibitor: None,
|
||||
client_for_window: Vec::new(),
|
||||
focused: None,
|
||||
qh,
|
||||
wayland_fd,
|
||||
read_guard: None,
|
||||
pending_events: VecDeque::new(),
|
||||
output_info: vec![],
|
||||
};
|
||||
|
||||
// dispatch registry to () again, in order to read all wl_outputs
|
||||
conn.display().get_registry(&state.qh, ());
|
||||
log::debug!("==============> requested registry");
|
||||
|
||||
// roundtrip to read wl_output globals
|
||||
queue.roundtrip(&mut state)?;
|
||||
log::debug!("==============> roundtrip 1 done");
|
||||
|
||||
// read outputs
|
||||
for output in state.g.outputs.iter() {
|
||||
state
|
||||
.g
|
||||
.xdg_output_manager
|
||||
.get_xdg_output(output, &state.qh, output.clone());
|
||||
}
|
||||
|
||||
// roundtrip to read xdg_output events
|
||||
queue.roundtrip(&mut state)?;
|
||||
|
||||
log::debug!("==============> roundtrip 2 done");
|
||||
for i in &state.output_info {
|
||||
log::debug!("{:#?}", i.1);
|
||||
}
|
||||
|
||||
let read_guard = loop {
|
||||
match queue.prepare_read() {
|
||||
Some(r) => break r,
|
||||
None => {
|
||||
queue.dispatch_pending(&mut state)?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
state.read_guard = Some(read_guard);
|
||||
|
||||
let inner = AsyncFd::new(Inner { queue, state })?;
|
||||
|
||||
Ok(WaylandEventProducer(inner))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,35 +467,17 @@ impl State {
|
||||
}
|
||||
|
||||
fn add_client(&mut self, client: ClientHandle, pos: Position) {
|
||||
let window = Rc::new(Window::new(&self.g, &self.qh, pos));
|
||||
self.client_for_window.push((window, client));
|
||||
let outputs = get_output_configuration(self, pos);
|
||||
|
||||
outputs.iter().for_each(|(o, i)| {
|
||||
let window = Window::new(self, &self.qh, o, pos, i.size);
|
||||
let window = Rc::new(window);
|
||||
self.client_for_window.push((window, client));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Source for WaylandEventProducer {
|
||||
fn register(
|
||||
&mut self,
|
||||
registry: &mio::Registry,
|
||||
token: mio::Token,
|
||||
interests: mio::Interest,
|
||||
) -> std::io::Result<()> {
|
||||
SourceFd(&self.state.wayland_fd).register(registry, token, interests)
|
||||
}
|
||||
|
||||
fn reregister(
|
||||
&mut self,
|
||||
registry: &mio::Registry,
|
||||
token: mio::Token,
|
||||
interests: mio::Interest,
|
||||
) -> std::io::Result<()> {
|
||||
SourceFd(&self.state.wayland_fd).reregister(registry, token, interests)
|
||||
}
|
||||
|
||||
fn deregister(&mut self, registry: &mio::Registry) -> std::io::Result<()> {
|
||||
SourceFd(&self.state.wayland_fd).deregister(registry)
|
||||
}
|
||||
}
|
||||
impl WaylandEventProducer {
|
||||
impl Inner {
|
||||
fn read(&mut self) -> bool {
|
||||
match self.state.read_guard.take().unwrap().read() {
|
||||
Ok(_) => true,
|
||||
@@ -343,16 +492,20 @@ impl WaylandEventProducer {
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_read(&mut self) {
|
||||
match self.queue.prepare_read() {
|
||||
Ok(r) => self.state.read_guard = Some(r),
|
||||
Err(WaylandError::Io(e)) => {
|
||||
log::error!("error preparing read from wayland socket: {e}")
|
||||
fn prepare_read(&mut self) -> io::Result<()> {
|
||||
loop {
|
||||
match self.queue.prepare_read() {
|
||||
None => match self.queue.dispatch_pending(&mut self.state) {
|
||||
Ok(_) => continue,
|
||||
Err(DispatchError::Backend(WaylandError::Io(e))) => return Err(e),
|
||||
Err(e) => panic!("failed to dispatch wayland events: {e}"),
|
||||
},
|
||||
Some(r) => {
|
||||
self.state.read_guard = Some(r);
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
Err(WaylandError::Protocol(e)) => {
|
||||
panic!("wayland Protocol violation: {e}")
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_events(&mut self) {
|
||||
@@ -381,46 +534,92 @@ impl WaylandEventProducer {
|
||||
Err(e) => match e {
|
||||
WaylandError::Io(e) => {
|
||||
log::error!("error writing to wayland socket: {e}")
|
||||
},
|
||||
}
|
||||
WaylandError::Protocol(e) => {
|
||||
panic!("wayland protocol violation: {e}")
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventProducer for WaylandEventProducer {
|
||||
|
||||
fn read_events(&mut self) -> Drain<(ClientHandle, Event)> {
|
||||
// read events
|
||||
while self.read() {
|
||||
// prepare next read
|
||||
self.prepare_read();
|
||||
}
|
||||
// dispatch the events
|
||||
self.dispatch_events();
|
||||
|
||||
// flush outgoing events
|
||||
self.flush_events();
|
||||
|
||||
// prepare for the next read
|
||||
self.prepare_read();
|
||||
|
||||
// return the events
|
||||
self.state.pending_events.drain(..)
|
||||
}
|
||||
|
||||
fn notify(&mut self, client_event: ClientEvent) {
|
||||
if let ClientEvent::Create(handle, pos) = client_event {
|
||||
self.state.add_client(handle, pos);
|
||||
self.flush_events();
|
||||
match client_event {
|
||||
ClientEvent::Create(handle, pos) => {
|
||||
self.0.get_mut().state.add_client(handle, pos);
|
||||
}
|
||||
ClientEvent::Destroy(handle) => {
|
||||
let inner = self.0.get_mut();
|
||||
// remove all windows corresponding to this client
|
||||
while let Some(i) = inner
|
||||
.state
|
||||
.client_for_window
|
||||
.iter()
|
||||
.position(|(_, c)| *c == handle)
|
||||
{
|
||||
inner.state.client_for_window.remove(i);
|
||||
inner.state.focused = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
let inner = self.0.get_mut();
|
||||
inner.flush_events();
|
||||
}
|
||||
|
||||
fn release(&mut self) {
|
||||
self.state.ungrab();
|
||||
self.flush_events();
|
||||
let inner = self.0.get_mut();
|
||||
inner.state.ungrab();
|
||||
inner.flush_events();
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for WaylandEventProducer {
|
||||
type Item = io::Result<(ClientHandle, Event)>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
if let Some(event) = self.0.get_mut().state.pending_events.pop_front() {
|
||||
return Poll::Ready(Some(Ok(event)));
|
||||
}
|
||||
|
||||
loop {
|
||||
let mut guard = ready!(self.0.poll_read_ready_mut(cx))?;
|
||||
|
||||
{
|
||||
let inner = guard.get_inner_mut();
|
||||
|
||||
// read events
|
||||
while inner.read() {
|
||||
// prepare next read
|
||||
match inner.prepare_read() {
|
||||
Ok(_) => {}
|
||||
Err(e) => return Poll::Ready(Some(Err(e))),
|
||||
}
|
||||
}
|
||||
|
||||
// dispatch the events
|
||||
inner.dispatch_events();
|
||||
|
||||
// flush outgoing events
|
||||
inner.flush_events();
|
||||
|
||||
// prepare for the next read
|
||||
match inner.prepare_read() {
|
||||
Ok(_) => {}
|
||||
Err(e) => return Poll::Ready(Some(Err(e))),
|
||||
}
|
||||
}
|
||||
|
||||
// clear read readiness for tokio read guard
|
||||
// guard.clear_ready_matching(Ready::READABLE);
|
||||
guard.clear_ready();
|
||||
|
||||
// if an event has been queued during dispatch_events() we return it
|
||||
match guard.get_inner_mut().state.pending_events.pop_front() {
|
||||
Some(event) => return Poll::Ready(Some(Ok(event))),
|
||||
None => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,7 +655,6 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
|
||||
_: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
) {
|
||||
|
||||
match event {
|
||||
wl_pointer::Event::Enter {
|
||||
serial,
|
||||
@@ -465,26 +663,26 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
|
||||
surface_y: _,
|
||||
} => {
|
||||
// get client corresponding to the focused surface
|
||||
log::trace!("produce: enter()");
|
||||
|
||||
{
|
||||
let (window, client) = app
|
||||
if let Some((window, client)) = app
|
||||
.client_for_window
|
||||
.iter()
|
||||
.find(|(w, _c)| w.surface == surface)
|
||||
.unwrap();
|
||||
app.focused = Some((window.clone(), *client));
|
||||
app.grab(&surface, pointer, serial.clone(), qh);
|
||||
{
|
||||
app.focused = Some((window.clone(), *client));
|
||||
app.grab(&surface, pointer, serial, qh);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let (_, client) = app
|
||||
.client_for_window
|
||||
.iter()
|
||||
.find(|(w, _c)| w.surface == surface)
|
||||
.unwrap();
|
||||
app.pending_events.push((*client, Event::Release()));
|
||||
app.pending_events.push_back((*client, Event::Enter()));
|
||||
}
|
||||
wl_pointer::Event::Leave { .. } => {
|
||||
log::trace!("produce: leave()");
|
||||
app.ungrab();
|
||||
}
|
||||
wl_pointer::Event::Button {
|
||||
@@ -493,9 +691,8 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
|
||||
button,
|
||||
state,
|
||||
} => {
|
||||
log::trace!("produce: button()");
|
||||
let (_, client) = app.focused.as_ref().unwrap();
|
||||
app.pending_events.push((
|
||||
app.pending_events.push_back((
|
||||
*client,
|
||||
Event::Pointer(PointerEvent::Button {
|
||||
time,
|
||||
@@ -505,9 +702,8 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
|
||||
));
|
||||
}
|
||||
wl_pointer::Event::Axis { time, axis, value } => {
|
||||
log::trace!("produce: scroll()");
|
||||
let (_, client) = app.focused.as_ref().unwrap();
|
||||
app.pending_events.push((
|
||||
app.pending_events.push_back((
|
||||
*client,
|
||||
Event::Pointer(PointerEvent::Axis {
|
||||
time,
|
||||
@@ -517,12 +713,9 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
|
||||
));
|
||||
}
|
||||
wl_pointer::Event::Frame {} => {
|
||||
log::trace!("produce: frame()");
|
||||
let (_, client) = app.focused.as_ref().unwrap();
|
||||
app.pending_events.push((
|
||||
*client,
|
||||
Event::Pointer(PointerEvent::Frame {}),
|
||||
));
|
||||
// TODO properly handle frame events
|
||||
// we simply insert a frame event on the client side
|
||||
// after each event for now
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -550,7 +743,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for State {
|
||||
state,
|
||||
} => {
|
||||
if let Some(client) = client {
|
||||
app.pending_events.push((
|
||||
app.pending_events.push_back((
|
||||
*client,
|
||||
Event::Keyboard(KeyboardEvent::Key {
|
||||
time,
|
||||
@@ -568,7 +761,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for State {
|
||||
group,
|
||||
} => {
|
||||
if let Some(client) = client {
|
||||
app.pending_events.push((
|
||||
app.pending_events.push_back((
|
||||
*client,
|
||||
Event::Keyboard(KeyboardEvent::Modifiers {
|
||||
mods_depressed,
|
||||
@@ -578,10 +771,6 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for State {
|
||||
}),
|
||||
));
|
||||
}
|
||||
if mods_depressed == 77 {
|
||||
// ctrl shift super alt
|
||||
app.ungrab();
|
||||
}
|
||||
}
|
||||
wl_keyboard::Event::Keymap {
|
||||
format: _,
|
||||
@@ -615,10 +804,9 @@ impl Dispatch<ZwpRelativePointerV1, ()> for State {
|
||||
dy_unaccel: surface_y,
|
||||
} = event
|
||||
{
|
||||
log::trace!("produce: motion()");
|
||||
if let Some((_window, client)) = &app.focused {
|
||||
let time = (((utime_hi as u64) << 32 | utime_lo as u64) / 1000) as u32;
|
||||
app.pending_events.push((
|
||||
app.pending_events.push_back((
|
||||
*client,
|
||||
Event::Pointer(PointerEvent::Motion {
|
||||
time,
|
||||
@@ -641,23 +829,23 @@ impl Dispatch<ZwlrLayerSurfaceV1, ()> for State {
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
if let zwlr_layer_surface_v1::Event::Configure { serial, .. } = event {
|
||||
let (window, _client) = app
|
||||
if let Some((window, _client)) = app
|
||||
.client_for_window
|
||||
.iter()
|
||||
.find(|(w, _c)| &w.layer_surface == layer_surface)
|
||||
.unwrap();
|
||||
// client corresponding to the layer_surface
|
||||
let surface = &window.surface;
|
||||
let buffer = &window.buffer;
|
||||
surface.attach(Some(&buffer), 0, 0);
|
||||
layer_surface.ack_configure(serial);
|
||||
surface.commit();
|
||||
{
|
||||
// client corresponding to the layer_surface
|
||||
let surface = &window.surface;
|
||||
let buffer = &window.buffer;
|
||||
surface.attach(Some(buffer), 0, 0);
|
||||
layer_surface.ack_configure(serial);
|
||||
surface.commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// delegate wl_registry events to App itself
|
||||
// delegate_dispatch!(App: [wl_registry::WlRegistry: GlobalListContents] => App);
|
||||
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for State {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
@@ -670,6 +858,71 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for State {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<wl_registry::WlRegistry, ()> for State {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
registry: &wl_registry::WlRegistry,
|
||||
event: <wl_registry::WlRegistry as wayland_client::Proxy>::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
wl_registry::Event::Global {
|
||||
name,
|
||||
interface,
|
||||
version: _,
|
||||
} => {
|
||||
if interface.as_str() == "wl_output" {
|
||||
log::debug!("wl_output global");
|
||||
state
|
||||
.g
|
||||
.outputs
|
||||
.push(registry.bind::<wl_output::WlOutput, _, _>(name, 4, qh, ()))
|
||||
}
|
||||
}
|
||||
wl_registry::Event::GlobalRemove { .. } => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<ZxdgOutputV1, WlOutput> for State {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_: &ZxdgOutputV1,
|
||||
event: <ZxdgOutputV1 as wayland_client::Proxy>::Event,
|
||||
wl_output: &WlOutput,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
log::debug!("xdg-output - {event:?}");
|
||||
let output_info = match state.output_info.iter_mut().find(|(o, _)| o == wl_output) {
|
||||
Some((_, c)) => c,
|
||||
None => {
|
||||
let output_info = OutputInfo::new();
|
||||
state.output_info.push((wl_output.clone(), output_info));
|
||||
&mut state.output_info.last_mut().unwrap().1
|
||||
}
|
||||
};
|
||||
|
||||
match event {
|
||||
zxdg_output_v1::Event::LogicalPosition { x, y } => {
|
||||
output_info.position = (x, y);
|
||||
}
|
||||
zxdg_output_v1::Event::LogicalSize { width, height } => {
|
||||
output_info.size = (width, height);
|
||||
}
|
||||
zxdg_output_v1::Event::Done => {}
|
||||
zxdg_output_v1::Event::Name { name } => {
|
||||
output_info.name = name;
|
||||
}
|
||||
zxdg_output_v1::Event::Description { .. } => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// don't emit any events
|
||||
delegate_noop!(State: wl_region::WlRegion);
|
||||
delegate_noop!(State: wl_shm_pool::WlShmPool);
|
||||
@@ -680,6 +933,8 @@ delegate_noop!(State: ZwpKeyboardShortcutsInhibitManagerV1);
|
||||
delegate_noop!(State: ZwpPointerConstraintsV1);
|
||||
|
||||
// ignore events
|
||||
delegate_noop!(State: ignore wl_output::WlOutput);
|
||||
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);
|
||||
|
||||
@@ -1,58 +1,31 @@
|
||||
use std::vec::Drain;
|
||||
|
||||
use mio::{Token, Registry};
|
||||
use mio::event::Source;
|
||||
use std::io::Result;
|
||||
use anyhow::{anyhow, Result};
|
||||
use core::task::{Context, Poll};
|
||||
use futures::Stream;
|
||||
use std::{io, pin::Pin};
|
||||
|
||||
use crate::{
|
||||
client::{ClientHandle, ClientEvent},
|
||||
client::{ClientEvent, ClientHandle},
|
||||
event::Event,
|
||||
producer::EventProducer,
|
||||
};
|
||||
|
||||
pub struct WindowsProducer {
|
||||
pending_events: Vec<(ClientHandle, Event)>,
|
||||
}
|
||||
|
||||
impl Source for WindowsProducer {
|
||||
fn register(
|
||||
&mut self,
|
||||
_registry: &Registry,
|
||||
_token: Token,
|
||||
_interests: mio::Interest,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reregister(
|
||||
&mut self,
|
||||
_registry: &Registry,
|
||||
_token: Token,
|
||||
_interests: mio::Interest,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn deregister(&mut self, _registry: &Registry) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WindowsProducer {}
|
||||
|
||||
impl EventProducer for WindowsProducer {
|
||||
fn notify(&mut self, _: ClientEvent) { }
|
||||
fn notify(&mut self, _: ClientEvent) {}
|
||||
|
||||
fn read_events(&mut self) -> Drain<(ClientHandle, Event)> {
|
||||
self.pending_events.drain(..)
|
||||
}
|
||||
|
||||
fn release(&mut self) { }
|
||||
fn release(&mut self) {}
|
||||
}
|
||||
|
||||
impl WindowsProducer {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
pending_events: vec![],
|
||||
}
|
||||
pub(crate) fn new() -> Result<Self> {
|
||||
Err(anyhow!("not implemented"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for WindowsProducer {
|
||||
type Item = io::Result<(ClientHandle, Event)>;
|
||||
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +1,35 @@
|
||||
use std::vec::Drain;
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::io;
|
||||
use std::task::Poll;
|
||||
|
||||
use mio::{Token, Registry};
|
||||
use mio::event::Source;
|
||||
use std::io::Result;
|
||||
use futures_core::Stream;
|
||||
|
||||
use crate::event::Event;
|
||||
use crate::producer::EventProducer;
|
||||
|
||||
use crate::{client::{ClientHandle, ClientEvent}, event::Event};
|
||||
use crate::client::{ClientEvent, ClientHandle};
|
||||
|
||||
pub struct X11Producer {
|
||||
pending_events: Vec<(ClientHandle, Event)>,
|
||||
}
|
||||
pub struct X11Producer {}
|
||||
|
||||
impl X11Producer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
pending_events: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Source for X11Producer {
|
||||
fn register(
|
||||
&mut self,
|
||||
_registry: &Registry,
|
||||
_token: Token,
|
||||
_interests: mio::Interest,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reregister(
|
||||
&mut self,
|
||||
_registry: &Registry,
|
||||
_token: Token,
|
||||
_interests: mio::Interest,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn deregister(&mut self, _registry: &Registry) -> Result<()> {
|
||||
Ok(())
|
||||
pub fn new() -> Result<Self> {
|
||||
Err(anyhow!("not implemented"))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventProducer for X11Producer {
|
||||
fn notify(&mut self, _: ClientEvent) { }
|
||||
|
||||
fn read_events(&mut self) -> Drain<(ClientHandle, Event)> {
|
||||
self.pending_events.drain(..)
|
||||
}
|
||||
fn notify(&mut self, _: ClientEvent) {}
|
||||
|
||||
fn release(&mut self) {}
|
||||
}
|
||||
|
||||
impl Stream for X11Producer {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
268
src/client.rs
268
src/client.rs
@@ -1,6 +1,11 @@
|
||||
use std::{net::SocketAddr, collections::{HashSet, hash_set::Iter}, fmt::Display, time::{Instant, Duration}, iter::Cloned};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fmt::Display,
|
||||
net::{IpAddr, SocketAddr},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum Position {
|
||||
@@ -9,20 +14,46 @@ pub enum Position {
|
||||
Top,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
impl Default for Position {
|
||||
fn default() -> Self {
|
||||
Self::Left
|
||||
}
|
||||
}
|
||||
|
||||
impl Position {
|
||||
pub fn opposite(&self) -> Self {
|
||||
match self {
|
||||
Position::Left => Self::Right,
|
||||
Position::Right => Self::Left,
|
||||
Position::Top => Self::Bottom,
|
||||
Position::Bottom => Self::Top,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Position {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", match self {
|
||||
Position::Left => "left",
|
||||
Position::Right => "right",
|
||||
Position::Top => "top",
|
||||
Position::Bottom => "bottom",
|
||||
})
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Position::Left => "left",
|
||||
Position::Right => "right",
|
||||
Position::Top => "top",
|
||||
Position::Bottom => "bottom",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub struct Client {
|
||||
/// handle to refer to the client.
|
||||
/// hostname of this client
|
||||
pub hostname: Option<String>,
|
||||
/// fix ips, determined by the user
|
||||
pub fix_ips: Vec<IpAddr>,
|
||||
/// unique handle to refer to the client.
|
||||
/// This way any event consumer / producer backend does not
|
||||
/// need to know anything about a client other than its handle.
|
||||
pub handle: ClientHandle,
|
||||
@@ -34,157 +65,142 @@ pub struct Client {
|
||||
/// e.g. Laptops usually have at least an ethernet and a wifi port
|
||||
/// which have different ip addresses
|
||||
pub addrs: HashSet<SocketAddr>,
|
||||
/// both active_addr and addrs can be None / empty so port needs to be stored seperately
|
||||
pub port: u16,
|
||||
/// position of a client on screen
|
||||
pub pos: Position,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ClientEvent {
|
||||
Create(ClientHandle, Position),
|
||||
Destroy(ClientHandle),
|
||||
UpdatePos(ClientHandle, Position),
|
||||
AddAddr(ClientHandle, SocketAddr),
|
||||
RemoveAddr(ClientHandle, SocketAddr),
|
||||
}
|
||||
|
||||
pub type ClientHandle = u32;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ClientState {
|
||||
pub client: Client,
|
||||
pub active: bool,
|
||||
pub last_ping: Option<Instant>,
|
||||
pub last_seen: Option<Instant>,
|
||||
pub last_replied: Option<Instant>,
|
||||
}
|
||||
|
||||
pub struct ClientManager {
|
||||
/// probably not beneficial to use a hashmap here
|
||||
clients: Vec<Client>,
|
||||
last_ping: Vec<(ClientHandle, Option<Instant>)>,
|
||||
last_seen: Vec<(ClientHandle, Option<Instant>)>,
|
||||
last_replied: Vec<(ClientHandle, Option<Instant>)>,
|
||||
next_client_id: u32,
|
||||
clients: Vec<Option<ClientState>>, // HashMap likely not beneficial
|
||||
}
|
||||
|
||||
impl Default for ClientManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
clients: vec![],
|
||||
next_client_id: 0,
|
||||
last_ping: vec![],
|
||||
last_seen: vec![],
|
||||
last_replied: vec![],
|
||||
}
|
||||
Self { clients: vec![] }
|
||||
}
|
||||
|
||||
/// add a new client to this manager
|
||||
pub fn add_client(&mut self, addrs: HashSet<SocketAddr>, pos: Position) -> ClientHandle {
|
||||
let handle = self.next_id();
|
||||
pub fn add_client(
|
||||
&mut self,
|
||||
hostname: Option<String>,
|
||||
ips: HashSet<IpAddr>,
|
||||
port: u16,
|
||||
pos: Position,
|
||||
) -> ClientHandle {
|
||||
// get a new client_handle
|
||||
let handle = self.free_id();
|
||||
|
||||
// we dont know, which IP is initially active
|
||||
let active_addr = None;
|
||||
|
||||
// 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 { handle, active_addr, addrs, pos };
|
||||
self.clients.push(client);
|
||||
self.last_ping.push((handle, None));
|
||||
self.last_seen.push((handle, None));
|
||||
self.last_replied.push((handle, None));
|
||||
let client = Client {
|
||||
hostname,
|
||||
fix_ips,
|
||||
handle,
|
||||
active_addr,
|
||||
addrs,
|
||||
port,
|
||||
pos,
|
||||
};
|
||||
|
||||
// client was never seen, nor pinged
|
||||
let client_state = ClientState {
|
||||
client,
|
||||
last_ping: None,
|
||||
last_seen: None,
|
||||
last_replied: None,
|
||||
active: false,
|
||||
};
|
||||
|
||||
if handle as usize >= self.clients.len() {
|
||||
assert_eq!(handle as usize, self.clients.len());
|
||||
self.clients.push(Some(client_state));
|
||||
} else {
|
||||
self.clients[handle as usize] = Some(client_state);
|
||||
}
|
||||
handle
|
||||
}
|
||||
|
||||
/// add a socket address to the given client
|
||||
pub fn add_addr(&mut self, client: ClientHandle, addr: SocketAddr) {
|
||||
if let Some(client) = self.get_mut(client) {
|
||||
client.addrs.insert(addr);
|
||||
}
|
||||
}
|
||||
|
||||
/// remove socket address from the given client
|
||||
pub fn remove_addr(&mut self, client: ClientHandle, addr: SocketAddr) {
|
||||
if let Some(client) = self.get_mut(client) {
|
||||
client.addrs.remove(&addr);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_default_addr(&mut self, client: ClientHandle, addr: SocketAddr) {
|
||||
if let Some(client) = self.get_mut(client) {
|
||||
client.active_addr = Some(addr)
|
||||
}
|
||||
}
|
||||
|
||||
/// update the position of a client
|
||||
pub fn update_pos(&mut self, client: ClientHandle, pos: Position) {
|
||||
if let Some(client) = self.get_mut(client) {
|
||||
client.pos = pos;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_active_addr(&self, client: ClientHandle) -> Option<SocketAddr> {
|
||||
self.get(client)?.active_addr
|
||||
}
|
||||
|
||||
pub fn get_addrs(&self, client: ClientHandle) -> Option<Cloned<Iter<'_, SocketAddr>>> {
|
||||
Some(self.get(client)?.addrs.iter().cloned())
|
||||
}
|
||||
|
||||
pub fn last_ping(&self, client: ClientHandle) -> Option<Duration> {
|
||||
let last_ping = self.last_ping
|
||||
.iter()
|
||||
.find(|(c,_)| *c == client)
|
||||
.unwrap().1;
|
||||
last_ping.map(|p| p.elapsed())
|
||||
}
|
||||
|
||||
pub fn last_seen(&self, client: ClientHandle) -> Option<Duration> {
|
||||
let last_seen = self.last_seen
|
||||
.iter()
|
||||
.find(|(c, _)| *c == client)
|
||||
.unwrap().1;
|
||||
last_seen.map(|t| t.elapsed())
|
||||
}
|
||||
|
||||
pub fn last_replied(&self, client: ClientHandle) -> Option<Duration> {
|
||||
let last_replied = self.last_replied
|
||||
.iter()
|
||||
.find(|(c, _)| *c == client)
|
||||
.unwrap().1;
|
||||
last_replied.map(|t| t.elapsed())
|
||||
}
|
||||
|
||||
pub fn reset_last_ping(&mut self, client: ClientHandle) {
|
||||
self.last_ping
|
||||
.iter_mut()
|
||||
.find(|(c, _)| *c == client)
|
||||
.unwrap().1 = Some(Instant::now());
|
||||
}
|
||||
|
||||
pub fn reset_last_seen(&mut self, client: ClientHandle) {
|
||||
self.last_seen
|
||||
.iter_mut()
|
||||
.find(|(c, _)| *c == client)
|
||||
.unwrap().1 = Some(Instant::now());
|
||||
}
|
||||
|
||||
pub fn reset_last_replied(&mut self, client: ClientHandle) {
|
||||
self.last_replied
|
||||
.iter_mut()
|
||||
.find(|(c, _)| *c == client)
|
||||
.unwrap().1 = Some(Instant::now());
|
||||
}
|
||||
|
||||
/// find a client by its address
|
||||
pub fn get_client(&self, addr: SocketAddr) -> Option<ClientHandle> {
|
||||
// since there shouldn't be more than a handful of clients at any given
|
||||
// time this is likely faster than using a HashMap
|
||||
self.clients
|
||||
.iter()
|
||||
.find(|c| c.addrs.contains(&addr))
|
||||
.map(|c| c.handle)
|
||||
.position(|c| {
|
||||
if let Some(c) = c {
|
||||
c.active && c.client.addrs.contains(&addr)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.map(|p| p as ClientHandle)
|
||||
}
|
||||
|
||||
fn next_id(&mut self) -> ClientHandle {
|
||||
let handle = self.next_client_id;
|
||||
self.next_client_id += 1;
|
||||
handle
|
||||
/// remove a client from the list
|
||||
pub fn remove_client(&mut self, client: ClientHandle) -> Option<ClientState> {
|
||||
// remove id from occupied ids
|
||||
self.clients.get_mut(client as usize)?.take()
|
||||
}
|
||||
|
||||
fn get<'a>(&'a self, client: ClientHandle) -> Option<&'a Client> {
|
||||
self.clients
|
||||
.iter()
|
||||
.find(|c| c.handle == client)
|
||||
/// get a free slot in the client list
|
||||
fn free_id(&mut self) -> ClientHandle {
|
||||
for i in 0..u32::MAX {
|
||||
if self.clients.get(i as usize).is_none()
|
||||
|| self.clients.get(i as usize).unwrap().is_none()
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
panic!("Out of client ids");
|
||||
}
|
||||
|
||||
fn get_mut<'a>(&'a mut self, client: ClientHandle) -> Option<&'a mut Client> {
|
||||
self.clients
|
||||
.iter_mut()
|
||||
.find(|c| c.handle == client)
|
||||
// returns an immutable reference to the client state corresponding to `client`
|
||||
pub fn get(&self, client: ClientHandle) -> Option<&ClientState> {
|
||||
self.clients.get(client as usize)?.as_ref()
|
||||
}
|
||||
|
||||
/// returns a mutable reference to the client state corresponding to `client`
|
||||
pub fn get_mut(&mut self, client: ClientHandle) -> Option<&mut ClientState> {
|
||||
self.clients.get_mut(client as usize)?.as_mut()
|
||||
}
|
||||
|
||||
pub fn get_client_states(&self) -> impl Iterator<Item = &ClientState> {
|
||||
self.clients.iter().filter_map(|x| x.as_ref())
|
||||
}
|
||||
|
||||
pub fn get_client_states_mut(&mut self) -> impl Iterator<Item = &mut ClientState> {
|
||||
self.clients.iter_mut().filter_map(|x| x.as_mut())
|
||||
}
|
||||
}
|
||||
|
||||
156
src/config.rs
156
src/config.rs
@@ -1,14 +1,13 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use core::fmt;
|
||||
use std::collections::HashSet;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::{error::Error, fs};
|
||||
|
||||
use std::env;
|
||||
use std::net::IpAddr;
|
||||
use std::{error::Error, fs};
|
||||
use toml;
|
||||
|
||||
use crate::client::Position;
|
||||
use crate::dns;
|
||||
|
||||
pub const DEFAULT_PORT: u16 = 4242;
|
||||
|
||||
@@ -29,65 +28,79 @@ pub struct Client {
|
||||
pub port: Option<u16>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct MissingParameter {
|
||||
arg: &'static str,
|
||||
}
|
||||
|
||||
impl fmt::Display for MissingParameter {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Missing a parameter for argument: {}", self.arg)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for MissingParameter {}
|
||||
|
||||
impl ConfigToml {
|
||||
pub fn new(path: &str) -> Result<ConfigToml, Box<dyn Error>> {
|
||||
let config = fs::read_to_string(path)?;
|
||||
log::info!("using config: \"{path}\"");
|
||||
Ok(toml::from_str::<_>(&config)?)
|
||||
}
|
||||
}
|
||||
|
||||
fn find_arg(key: &'static str) -> Result<Option<String>, MissingParameter> {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct CliArgs {
|
||||
/// the listen port for lan-mouse
|
||||
#[arg(short, long)]
|
||||
port: Option<u16>,
|
||||
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
if arg != key {
|
||||
continue;
|
||||
}
|
||||
match args.get(i+1) {
|
||||
None => return Err(MissingParameter { arg: key }),
|
||||
Some(arg) => return Ok(Some(arg.clone())),
|
||||
};
|
||||
}
|
||||
Ok(None)
|
||||
/// the frontend to use [cli | gtk]
|
||||
#[arg(short, long)]
|
||||
frontend: Option<String>,
|
||||
|
||||
/// non-default config file location
|
||||
#[arg(short, long)]
|
||||
config: Option<String>,
|
||||
|
||||
/// run only the service as a daemon without the frontend
|
||||
#[arg(short, long)]
|
||||
daemon: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Frontend {
|
||||
Gtk,
|
||||
Cli,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Config {
|
||||
pub frontend: Frontend,
|
||||
pub port: u16,
|
||||
pub clients: Vec<(Client, Position)>,
|
||||
pub daemon: bool,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new() -> Result<Self, Box<dyn Error>> {
|
||||
let config_path = "config.toml";
|
||||
let config_toml = match ConfigToml::new(config_path) {
|
||||
pub fn new() -> Result<Self> {
|
||||
let args = CliArgs::parse();
|
||||
let config_file = "config.toml";
|
||||
#[cfg(unix)]
|
||||
let config_path = {
|
||||
let xdg_config_home =
|
||||
env::var("XDG_CONFIG_HOME").unwrap_or(format!("{}/.config", env::var("HOME")?));
|
||||
format!("{xdg_config_home}/lan-mouse/{config_file}")
|
||||
};
|
||||
|
||||
#[cfg(not(unix))]
|
||||
let config_path = {
|
||||
let app_data =
|
||||
env::var("LOCALAPPDATA").unwrap_or(format!("{}/.config", env::var("USERPROFILE")?));
|
||||
format!("{app_data}\\lan-mouse\\{config_file}")
|
||||
};
|
||||
|
||||
// --config <file> overrules default location
|
||||
let config_path = args.config.unwrap_or(config_path);
|
||||
|
||||
let config_toml = match ConfigToml::new(config_path.as_str()) {
|
||||
Err(e) => {
|
||||
log::error!("config.toml: {e}");
|
||||
log::error!("{config_path}: {e}");
|
||||
log::warn!("Continuing without config file ...");
|
||||
None
|
||||
},
|
||||
}
|
||||
Ok(c) => Some(c),
|
||||
};
|
||||
|
||||
let frontend = match find_arg("--frontend")? {
|
||||
let frontend = match args.frontend {
|
||||
None => match &config_toml {
|
||||
Some(c) => c.frontend.clone(),
|
||||
None => None,
|
||||
@@ -96,20 +109,23 @@ impl Config {
|
||||
};
|
||||
|
||||
let frontend = match frontend {
|
||||
#[cfg(feature = "gtk")]
|
||||
None => Frontend::Gtk,
|
||||
#[cfg(not(feature = "gtk"))]
|
||||
None => Frontend::Cli,
|
||||
Some(s) => match s.as_str() {
|
||||
"cli" => Frontend::Cli,
|
||||
"gtk" => Frontend::Gtk,
|
||||
_ => Frontend::Cli,
|
||||
}
|
||||
_ => Frontend::Cli,
|
||||
},
|
||||
};
|
||||
|
||||
let port = match find_arg("--port")? {
|
||||
Some(port) => port.parse::<u16>()?,
|
||||
let port = match args.port {
|
||||
Some(port) => port,
|
||||
None => match &config_toml {
|
||||
Some(c) => c.port.unwrap_or(DEFAULT_PORT),
|
||||
None => DEFAULT_PORT,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let mut clients: Vec<(Client, Position)> = vec![];
|
||||
@@ -129,43 +145,29 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Config { frontend, clients, port })
|
||||
let daemon = args.daemon;
|
||||
|
||||
Ok(Config {
|
||||
daemon,
|
||||
frontend,
|
||||
clients,
|
||||
port,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_clients(&self) -> Vec<(HashSet<SocketAddr>, Option<String>, Position)> {
|
||||
self.clients.iter().map(|(c,p)| {
|
||||
let port = c.port.unwrap_or(DEFAULT_PORT);
|
||||
// add ips from config
|
||||
let config_ips: Vec<IpAddr> = if let Some(ips) = c.ips.as_ref() {
|
||||
ips.iter().cloned().collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let host_name = c.host_name.clone();
|
||||
// add ips from dns lookup
|
||||
let dns_ips = match host_name.as_ref() {
|
||||
None => vec![],
|
||||
Some(host_name) => match dns::resolve(host_name) {
|
||||
Err(e) => {
|
||||
log::warn!("{host_name}: could not resolve host: {e}");
|
||||
vec![]
|
||||
}
|
||||
Ok(l) if l.is_empty() => {
|
||||
log::warn!("{host_name}: could not resolve host");
|
||||
vec![]
|
||||
}
|
||||
Ok(l) => l,
|
||||
}
|
||||
};
|
||||
if config_ips.is_empty() && dns_ips.is_empty() {
|
||||
log::error!("no ips found for client {p:?}, ignoring!");
|
||||
log::error!("You can manually specify ip addresses via the `ips` config option");
|
||||
}
|
||||
let ips = config_ips.into_iter().chain(dns_ips.into_iter());
|
||||
|
||||
// map ip addresses to socket addresses
|
||||
let addrs: HashSet<SocketAddr> = ips.map(|ip| SocketAddr::new(ip, port)).collect();
|
||||
(addrs, host_name, *p)
|
||||
}).filter(|(a, _, _)| !a.is_empty()).collect()
|
||||
pub fn get_clients(&self) -> Vec<(HashSet<IpAddr>, Option<String>, u16, Position)> {
|
||||
self.clients
|
||||
.iter()
|
||||
.map(|(c, p)| {
|
||||
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)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
167
src/consumer.rs
167
src/consumer.rs
@@ -1,99 +1,78 @@
|
||||
#[cfg(unix)]
|
||||
use std::env;
|
||||
use async_trait::async_trait;
|
||||
use std::future;
|
||||
|
||||
use crate::{
|
||||
backend::consumer,
|
||||
client::{ClientEvent, ClientHandle},
|
||||
event::Event,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use crate::{backend::consumer, client::{ClientHandle, ClientEvent}, event::Event};
|
||||
|
||||
#[cfg(unix)]
|
||||
#[derive(Debug)]
|
||||
enum Backend {
|
||||
Wlroots,
|
||||
X11,
|
||||
RemoteDesktopPortal,
|
||||
Libei,
|
||||
}
|
||||
|
||||
pub trait EventConsumer {
|
||||
/// Event corresponding to an abstract `client_handle`
|
||||
fn consume(&self, event: Event, client_handle: ClientHandle);
|
||||
|
||||
/// Event corresponding to a configuration change
|
||||
fn notify(&mut self, client_event: ClientEvent);
|
||||
}
|
||||
|
||||
pub fn create() -> Result<Box<dyn EventConsumer>> {
|
||||
#[cfg(windows)]
|
||||
return Ok(Box::new(consumer::windows::WindowsConsumer::new()));
|
||||
|
||||
#[cfg(unix)]
|
||||
let backend = match env::var("XDG_SESSION_TYPE") {
|
||||
Ok(session_type) => match session_type.as_str() {
|
||||
"x11" => {
|
||||
log::info!("XDG_SESSION_TYPE = x11 -> using x11 event consumer");
|
||||
Backend::X11
|
||||
}
|
||||
"wayland" => {
|
||||
log::info!("XDG_SESSION_TYPE = wayland -> using wayland event consumer");
|
||||
match env::var("XDG_CURRENT_DESKTOP") {
|
||||
Ok(current_desktop) => match current_desktop.as_str() {
|
||||
"gnome" => {
|
||||
log::info!("XDG_CURRENT_DESKTOP = gnome -> using libei backend");
|
||||
Backend::Libei
|
||||
}
|
||||
"KDE" => {
|
||||
log::info!("XDG_CURRENT_DESKTOP = KDE -> using xdg_desktop_portal backend");
|
||||
Backend::RemoteDesktopPortal
|
||||
}
|
||||
"sway" => {
|
||||
log::info!("XDG_CURRENT_DESKTOP = sway -> using wlroots backend");
|
||||
Backend::Wlroots
|
||||
}
|
||||
"Hyprland" => {
|
||||
log::info!("XDG_CURRENT_DESKTOP = Hyprland -> using wlroots backend");
|
||||
Backend::Wlroots
|
||||
}
|
||||
_ => {
|
||||
log::warn!("unknown XDG_CURRENT_DESKTOP -> defaulting to wlroots backend");
|
||||
Backend::Wlroots
|
||||
}
|
||||
}
|
||||
// default to wlroots backend for now
|
||||
_ => {
|
||||
log::warn!("unknown XDG_CURRENT_DESKTOP -> defaulting to wlroots backend");
|
||||
Backend::Wlroots
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => panic!("unknown XDG_SESSION_TYPE"),
|
||||
},
|
||||
Err(_) => panic!("could not detect session type: XDG_SESSION_TYPE environment variable not set!"),
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
match backend {
|
||||
Backend::Libei => {
|
||||
#[cfg(not(feature = "libei"))]
|
||||
panic!("feature libei not enabled");
|
||||
#[cfg(feature = "libei")]
|
||||
Ok(Box::new(consumer::libei::LibeiConsumer::new()))
|
||||
},
|
||||
Backend::RemoteDesktopPortal => {
|
||||
#[cfg(not(feature = "xdg_desktop_portal"))]
|
||||
panic!("feature xdg_desktop_portal not enabled");
|
||||
#[cfg(feature = "xdg_desktop_portal")]
|
||||
Ok(Box::new(consumer::xdg_desktop_portal::DesktopPortalConsumer::new()))
|
||||
},
|
||||
Backend::Wlroots => {
|
||||
#[cfg(not(feature = "wayland"))]
|
||||
panic!("feature wayland not enabled");
|
||||
#[cfg(feature = "wayland")]
|
||||
Ok(Box::new(consumer::wlroots::WlrootsConsumer::new()?))
|
||||
},
|
||||
Backend::X11 => {
|
||||
#[cfg(not(feature = "x11"))]
|
||||
panic!("feature x11 not enabled");
|
||||
#[cfg(feature = "x11")]
|
||||
Ok(Box::new(consumer::x11::X11Consumer::new()))
|
||||
},
|
||||
#[async_trait]
|
||||
pub trait EventConsumer: Send {
|
||||
async fn consume(&mut self, event: Event, client_handle: ClientHandle);
|
||||
async fn notify(&mut self, client_event: ClientEvent);
|
||||
/// this function is waited on continuously and can be used to handle events
|
||||
async fn dispatch(&mut self) -> Result<()> {
|
||||
let _: () = future::pending().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn destroy(&mut self);
|
||||
}
|
||||
|
||||
pub async fn create() -> Box<dyn EventConsumer> {
|
||||
#[cfg(windows)]
|
||||
match consumer::windows::WindowsConsumer::new() {
|
||||
Ok(c) => return Box::new(c),
|
||||
Err(e) => log::warn!("windows event consumer unavailable: {e}"),
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
match consumer::macos::MacOSConsumer::new() {
|
||||
Ok(c) => {
|
||||
log::info!("using macos event consumer");
|
||||
return Box::new(c);
|
||||
}
|
||||
Err(e) => log::error!("macos consumer not available: {e}"),
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
match consumer::wlroots::WlrootsConsumer::new() {
|
||||
Ok(c) => {
|
||||
log::info!("using wlroots event consumer");
|
||||
return Box::new(c);
|
||||
}
|
||||
Err(e) => log::info!("wayland backend not available: {e}"),
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
match consumer::libei::LibeiConsumer::new().await {
|
||||
Ok(c) => {
|
||||
log::info!("using libei event consumer");
|
||||
return Box::new(c);
|
||||
}
|
||||
Err(e) => log::info!("libei not available: {e}"),
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
|
||||
match consumer::xdg_desktop_portal::DesktopPortalConsumer::new().await {
|
||||
Ok(c) => {
|
||||
log::info!("using xdg-remote-desktop-portal event consumer");
|
||||
return Box::new(c);
|
||||
}
|
||||
Err(e) => log::info!("remote desktop portal not available: {e}"),
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
match consumer::x11::X11Consumer::new() {
|
||||
Ok(c) => {
|
||||
log::info!("using x11 event consumer");
|
||||
return Box::new(c);
|
||||
}
|
||||
Err(e) => log::info!("x11 consumer not available: {e}"),
|
||||
}
|
||||
|
||||
log::error!("falling back to dummy event consumer");
|
||||
Box::new(consumer::dummy::DummyConsumer::new())
|
||||
}
|
||||
|
||||
24
src/dns.rs
24
src/dns.rs
@@ -1,9 +1,23 @@
|
||||
use anyhow::Result;
|
||||
use std::{error::Error, net::IpAddr};
|
||||
|
||||
use trust_dns_resolver::Resolver;
|
||||
use trust_dns_resolver::TokioAsyncResolver;
|
||||
|
||||
pub fn resolve(host: &str) -> Result<Vec<IpAddr>, Box<dyn Error>> {
|
||||
log::info!("resolving {host} ...");
|
||||
let response = Resolver::from_system_conf()?.lookup_ip(host)?;
|
||||
Ok(response.iter().collect())
|
||||
pub struct DnsResolver {
|
||||
resolver: TokioAsyncResolver,
|
||||
}
|
||||
impl DnsResolver {
|
||||
pub(crate) async fn new() -> Result<Self> {
|
||||
let resolver = TokioAsyncResolver::tokio_from_system_conf()?;
|
||||
Ok(Self { resolver })
|
||||
}
|
||||
|
||||
pub(crate) async fn resolve(&self, host: &str) -> Result<Vec<IpAddr>, Box<dyn Error>> {
|
||||
log::info!("resolving {host} ...");
|
||||
let response = self.resolver.lookup_ip(host).await?;
|
||||
for ip in response.iter() {
|
||||
log::info!("{host}: adding ip {ip}");
|
||||
}
|
||||
Ok(response.iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
190
src/event.rs
190
src/event.rs
@@ -1,6 +1,14 @@
|
||||
use std::{error::Error, fmt::{self, Display}};
|
||||
use std::{
|
||||
error::Error,
|
||||
fmt::{self, Display},
|
||||
};
|
||||
|
||||
pub mod server;
|
||||
// FIXME
|
||||
pub const BTN_LEFT: u32 = 0x110;
|
||||
pub const BTN_RIGHT: u32 = 0x111;
|
||||
pub const BTN_MIDDLE: u32 = 0x112;
|
||||
pub const BTN_BACK: u32 = 0x113;
|
||||
pub const BTN_FORWARD: u32 = 0x114;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum PointerEvent {
|
||||
@@ -39,20 +47,45 @@ pub enum KeyboardEvent {
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Event {
|
||||
/// pointer event (motion / button / axis)
|
||||
Pointer(PointerEvent),
|
||||
/// keyboard events (key / modifiers)
|
||||
Keyboard(KeyboardEvent),
|
||||
Release(),
|
||||
/// enter event: request to enter a client.
|
||||
/// The client must release the pointer if it is grabbed
|
||||
/// and reply with a leave event, as soon as its ready to
|
||||
/// receive events
|
||||
Enter(),
|
||||
/// leave event: this client is now ready to receive events and will
|
||||
/// not send any events after until it sends an enter event
|
||||
Leave(),
|
||||
/// ping a client, to see if it is still alive. A client that does
|
||||
/// not respond with a pong event will be assumed to be offline.
|
||||
Ping(),
|
||||
/// response to a ping event: this event signals that a client
|
||||
/// is still alive but must otherwise be ignored
|
||||
Pong(),
|
||||
}
|
||||
|
||||
impl Display for PointerEvent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
PointerEvent::Motion { time: _ , relative_x, relative_y } => write!(f, "motion({relative_x},{relative_y})"),
|
||||
PointerEvent::Button { time: _ , button, state } => write!(f, "button({button}, {state})"),
|
||||
PointerEvent::Axis { time: _, axis, value } => write!(f, "scroll({axis}, {value})"),
|
||||
PointerEvent::Frame { } => write!(f, "frame()"),
|
||||
PointerEvent::Motion {
|
||||
time: _,
|
||||
relative_x,
|
||||
relative_y,
|
||||
} => write!(f, "motion({relative_x},{relative_y})"),
|
||||
PointerEvent::Button {
|
||||
time: _,
|
||||
button,
|
||||
state,
|
||||
} => write!(f, "button({button}, {state})"),
|
||||
PointerEvent::Axis {
|
||||
time: _,
|
||||
axis,
|
||||
value,
|
||||
} => write!(f, "scroll({axis}, {value})"),
|
||||
PointerEvent::Frame {} => write!(f, "frame()"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,8 +93,20 @@ impl Display for PointerEvent {
|
||||
impl Display for KeyboardEvent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
KeyboardEvent::Key { time: _, key, state } => write!(f, "key({key}, {state})"),
|
||||
KeyboardEvent::Modifiers { mods_depressed, mods_latched, mods_locked, group } => write!(f, "modifiers({mods_depressed},{mods_latched},{mods_locked},{group})"),
|
||||
KeyboardEvent::Key {
|
||||
time: _,
|
||||
key,
|
||||
state,
|
||||
} => write!(f, "key({key}, {state})"),
|
||||
KeyboardEvent::Modifiers {
|
||||
mods_depressed,
|
||||
mods_latched,
|
||||
mods_locked,
|
||||
group,
|
||||
} => write!(
|
||||
f,
|
||||
"modifiers({mods_depressed},{mods_latched},{mods_locked},{group})"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,7 +116,8 @@ impl Display for Event {
|
||||
match self {
|
||||
Event::Pointer(p) => write!(f, "{}", p),
|
||||
Event::Keyboard(k) => write!(f, "{}", k),
|
||||
Event::Release() => write!(f, "release"),
|
||||
Event::Enter() => write!(f, "enter"),
|
||||
Event::Leave() => write!(f, "leave"),
|
||||
Event::Ping() => write!(f, "ping"),
|
||||
Event::Pong() => write!(f, "pong"),
|
||||
}
|
||||
@@ -81,11 +127,12 @@ impl Display for Event {
|
||||
impl Event {
|
||||
fn event_type(&self) -> EventType {
|
||||
match self {
|
||||
Self::Pointer(_) => EventType::POINTER,
|
||||
Self::Keyboard(_) => EventType::KEYBOARD,
|
||||
Self::Release() => EventType::RELEASE,
|
||||
Self::Ping() => EventType::PING,
|
||||
Self::Pong() => EventType::PONG,
|
||||
Self::Pointer(_) => EventType::Pointer,
|
||||
Self::Keyboard(_) => EventType::Keyboard,
|
||||
Self::Enter() => EventType::Enter,
|
||||
Self::Leave() => EventType::Leave,
|
||||
Self::Ping() => EventType::Ping,
|
||||
Self::Pong() => EventType::Pong,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,10 +140,10 @@ impl Event {
|
||||
impl PointerEvent {
|
||||
fn event_type(&self) -> PointerEventType {
|
||||
match self {
|
||||
Self::Motion { .. } => PointerEventType::MOTION,
|
||||
Self::Button { .. } => PointerEventType::BUTTON,
|
||||
Self::Axis { .. } => PointerEventType::AXIS,
|
||||
Self::Frame { .. } => PointerEventType::FRAME,
|
||||
Self::Motion { .. } => PointerEventType::Motion,
|
||||
Self::Button { .. } => PointerEventType::Button,
|
||||
Self::Axis { .. } => PointerEventType::Axis,
|
||||
Self::Frame { .. } => PointerEventType::Frame,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,28 +151,29 @@ impl PointerEvent {
|
||||
impl KeyboardEvent {
|
||||
fn event_type(&self) -> KeyboardEventType {
|
||||
match self {
|
||||
KeyboardEvent::Key { .. } => KeyboardEventType::KEY,
|
||||
KeyboardEvent::Modifiers { .. } => KeyboardEventType::MODIFIERS,
|
||||
KeyboardEvent::Key { .. } => KeyboardEventType::Key,
|
||||
KeyboardEvent::Modifiers { .. } => KeyboardEventType::Modifiers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum PointerEventType {
|
||||
MOTION,
|
||||
BUTTON,
|
||||
AXIS,
|
||||
FRAME,
|
||||
Motion,
|
||||
Button,
|
||||
Axis,
|
||||
Frame,
|
||||
}
|
||||
enum KeyboardEventType {
|
||||
KEY,
|
||||
MODIFIERS,
|
||||
Key,
|
||||
Modifiers,
|
||||
}
|
||||
enum EventType {
|
||||
POINTER,
|
||||
KEYBOARD,
|
||||
RELEASE,
|
||||
PING,
|
||||
PONG,
|
||||
Pointer,
|
||||
Keyboard,
|
||||
Enter,
|
||||
Leave,
|
||||
Ping,
|
||||
Pong,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for PointerEventType {
|
||||
@@ -133,10 +181,10 @@ impl TryFrom<u8> for PointerEventType {
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
x if x == Self::MOTION as u8 => Ok(Self::MOTION),
|
||||
x if x == Self::BUTTON as u8 => Ok(Self::BUTTON),
|
||||
x if x == Self::AXIS as u8 => Ok(Self::AXIS),
|
||||
x if x == Self::FRAME as u8 => Ok(Self::FRAME),
|
||||
x if x == Self::Motion as u8 => Ok(Self::Motion),
|
||||
x if x == Self::Button as u8 => Ok(Self::Button),
|
||||
x if x == Self::Axis as u8 => Ok(Self::Axis),
|
||||
x if x == Self::Frame as u8 => Ok(Self::Frame),
|
||||
_ => Err(Box::new(ProtocolError {
|
||||
msg: format!("invalid pointer event type {}", value),
|
||||
})),
|
||||
@@ -149,8 +197,8 @@ impl TryFrom<u8> for KeyboardEventType {
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
x if x == Self::KEY as u8 => Ok(Self::KEY),
|
||||
x if x == Self::MODIFIERS as u8 => Ok(Self::MODIFIERS),
|
||||
x if x == Self::Key as u8 => Ok(Self::Key),
|
||||
x if x == Self::Modifiers as u8 => Ok(Self::Modifiers),
|
||||
_ => Err(Box::new(ProtocolError {
|
||||
msg: format!("invalid keyboard event type {}", value),
|
||||
})),
|
||||
@@ -158,17 +206,18 @@ impl TryFrom<u8> for KeyboardEventType {
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Vec<u8>> for &Event {
|
||||
fn into(self) -> Vec<u8> {
|
||||
let event_id = vec![self.event_type() as u8];
|
||||
let event_data = match self {
|
||||
impl From<&Event> for Vec<u8> {
|
||||
fn from(event: &Event) -> Self {
|
||||
let event_id = vec![event.event_type() as u8];
|
||||
let event_data = match event {
|
||||
Event::Pointer(p) => p.into(),
|
||||
Event::Keyboard(k) => k.into(),
|
||||
Event::Release() => vec![],
|
||||
Event::Enter() => vec![],
|
||||
Event::Leave() => vec![],
|
||||
Event::Ping() => vec![],
|
||||
Event::Pong() => vec![],
|
||||
};
|
||||
vec![event_id, event_data].concat()
|
||||
[event_id, event_data].concat()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,11 +239,12 @@ impl TryFrom<Vec<u8>> for Event {
|
||||
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
|
||||
let event_id = u8::from_be_bytes(value[..1].try_into()?);
|
||||
match event_id {
|
||||
i if i == (EventType::POINTER as u8) => Ok(Event::Pointer(value.try_into()?)),
|
||||
i if i == (EventType::KEYBOARD as u8) => Ok(Event::Keyboard(value.try_into()?)),
|
||||
i if i == (EventType::RELEASE as u8) => Ok(Event::Release()),
|
||||
i if i == (EventType::PING as u8) => Ok(Event::Ping()),
|
||||
i if i == (EventType::PONG as u8) => Ok(Event::Pong()),
|
||||
i if i == (EventType::Pointer as u8) => Ok(Event::Pointer(value.try_into()?)),
|
||||
i if i == (EventType::Keyboard as u8) => Ok(Event::Keyboard(value.try_into()?)),
|
||||
i if i == (EventType::Enter as u8) => Ok(Event::Enter()),
|
||||
i if i == (EventType::Leave as u8) => Ok(Event::Leave()),
|
||||
i if i == (EventType::Ping as u8) => Ok(Event::Ping()),
|
||||
i if i == (EventType::Pong as u8) => Ok(Event::Pong()),
|
||||
_ => Err(Box::new(ProtocolError {
|
||||
msg: format!("invalid event_id {}", event_id),
|
||||
})),
|
||||
@@ -202,10 +252,10 @@ impl TryFrom<Vec<u8>> for Event {
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Vec<u8>> for &PointerEvent {
|
||||
fn into(self) -> Vec<u8> {
|
||||
let id = vec![self.event_type() as u8];
|
||||
let data = match self {
|
||||
impl From<&PointerEvent> for Vec<u8> {
|
||||
fn from(event: &PointerEvent) -> Self {
|
||||
let id = vec![event.event_type() as u8];
|
||||
let data = match event {
|
||||
PointerEvent::Motion {
|
||||
time,
|
||||
relative_x,
|
||||
@@ -214,7 +264,7 @@ impl Into<Vec<u8>> for &PointerEvent {
|
||||
let time = time.to_be_bytes();
|
||||
let relative_x = relative_x.to_be_bytes();
|
||||
let relative_y = relative_y.to_be_bytes();
|
||||
vec![&time[..], &relative_x[..], &relative_y[..]].concat()
|
||||
[&time[..], &relative_x[..], &relative_y[..]].concat()
|
||||
}
|
||||
PointerEvent::Button {
|
||||
time,
|
||||
@@ -224,19 +274,19 @@ impl Into<Vec<u8>> for &PointerEvent {
|
||||
let time = time.to_be_bytes();
|
||||
let button = button.to_be_bytes();
|
||||
let state = state.to_be_bytes();
|
||||
vec![&time[..], &button[..], &state[..]].concat()
|
||||
[&time[..], &button[..], &state[..]].concat()
|
||||
}
|
||||
PointerEvent::Axis { time, axis, value } => {
|
||||
let time = time.to_be_bytes();
|
||||
let axis = axis.to_be_bytes();
|
||||
let value = value.to_be_bytes();
|
||||
vec![&time[..], &axis[..], &value[..]].concat()
|
||||
[&time[..], &axis[..], &value[..]].concat()
|
||||
}
|
||||
PointerEvent::Frame {} => {
|
||||
vec![]
|
||||
}
|
||||
};
|
||||
vec![id, data].concat()
|
||||
[id, data].concat()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,7 +301,7 @@ impl TryFrom<Vec<u8>> for PointerEvent {
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
match event_type {
|
||||
PointerEventType::MOTION => {
|
||||
PointerEventType::Motion => {
|
||||
let time = match data.get(2..6) {
|
||||
Some(d) => u32::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
@@ -282,7 +332,7 @@ impl TryFrom<Vec<u8>> for PointerEvent {
|
||||
relative_y,
|
||||
})
|
||||
}
|
||||
PointerEventType::BUTTON => {
|
||||
PointerEventType::Button => {
|
||||
let time = match data.get(2..6) {
|
||||
Some(d) => u32::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
@@ -313,7 +363,7 @@ impl TryFrom<Vec<u8>> for PointerEvent {
|
||||
state,
|
||||
})
|
||||
}
|
||||
PointerEventType::AXIS => {
|
||||
PointerEventType::Axis => {
|
||||
let time = match data.get(2..6) {
|
||||
Some(d) => u32::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
@@ -340,7 +390,7 @@ impl TryFrom<Vec<u8>> for PointerEvent {
|
||||
};
|
||||
Ok(Self::Axis { time, axis, value })
|
||||
}
|
||||
PointerEventType::FRAME => Ok(Self::Frame {}),
|
||||
PointerEventType::Frame => Ok(Self::Frame {}),
|
||||
}
|
||||
}
|
||||
None => Err(Box::new(ProtocolError {
|
||||
@@ -350,15 +400,15 @@ impl TryFrom<Vec<u8>> for PointerEvent {
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Vec<u8>> for &KeyboardEvent {
|
||||
fn into(self) -> Vec<u8> {
|
||||
let id = vec![self.event_type() as u8];
|
||||
let data = match self {
|
||||
impl From<&KeyboardEvent> for Vec<u8> {
|
||||
fn from(event: &KeyboardEvent) -> Self {
|
||||
let id = vec![event.event_type() as u8];
|
||||
let data = match event {
|
||||
KeyboardEvent::Key { time, key, state } => {
|
||||
let time = time.to_be_bytes();
|
||||
let key = key.to_be_bytes();
|
||||
let state = state.to_be_bytes();
|
||||
vec![&time[..], &key[..], &state[..]].concat()
|
||||
[&time[..], &key[..], &state[..]].concat()
|
||||
}
|
||||
KeyboardEvent::Modifiers {
|
||||
mods_depressed,
|
||||
@@ -370,7 +420,7 @@ impl Into<Vec<u8>> for &KeyboardEvent {
|
||||
let mods_latched = mods_latched.to_be_bytes();
|
||||
let mods_locked = mods_locked.to_be_bytes();
|
||||
let group = group.to_be_bytes();
|
||||
vec![
|
||||
[
|
||||
&mods_depressed[..],
|
||||
&mods_latched[..],
|
||||
&mods_locked[..],
|
||||
@@ -379,7 +429,7 @@ impl Into<Vec<u8>> for &KeyboardEvent {
|
||||
.concat()
|
||||
}
|
||||
};
|
||||
vec![id, data].concat()
|
||||
[id, data].concat()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,7 +444,7 @@ impl TryFrom<Vec<u8>> for KeyboardEvent {
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
match event_type {
|
||||
KeyboardEventType::KEY => {
|
||||
KeyboardEventType::Key => {
|
||||
let time = match data.get(2..6) {
|
||||
Some(d) => u32::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
@@ -421,7 +471,7 @@ impl TryFrom<Vec<u8>> for KeyboardEvent {
|
||||
};
|
||||
Ok(KeyboardEvent::Key { time, key, state })
|
||||
}
|
||||
KeyboardEventType::MODIFIERS => {
|
||||
KeyboardEventType::Modifiers => {
|
||||
let mods_depressed = match data.get(2..6) {
|
||||
Some(d) => u32::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
|
||||
@@ -1,282 +0,0 @@
|
||||
use std::{error::Error, io::Result, collections::HashSet, time::Duration};
|
||||
use log;
|
||||
use mio::{Events, Poll, Interest, Token, net::UdpSocket};
|
||||
#[cfg(not(windows))]
|
||||
use mio_signals::{Signals, Signal, SignalSet};
|
||||
|
||||
use std::{net::SocketAddr, io::ErrorKind};
|
||||
|
||||
use crate::{client::{ClientEvent, ClientManager, Position}, consumer::EventConsumer, producer::EventProducer, frontend::{FrontendEvent, FrontendAdapter}};
|
||||
use super::Event;
|
||||
|
||||
/// keeps track of state to prevent a feedback loop
|
||||
/// of continuously sending and receiving the same event.
|
||||
#[derive(Eq, PartialEq)]
|
||||
enum State {
|
||||
Sending,
|
||||
Receiving,
|
||||
}
|
||||
|
||||
pub struct Server {
|
||||
poll: Poll,
|
||||
socket: UdpSocket,
|
||||
producer: Box<dyn EventProducer>,
|
||||
consumer: Box<dyn EventConsumer>,
|
||||
#[cfg(not(windows))]
|
||||
signals: Signals,
|
||||
frontend: FrontendAdapter,
|
||||
client_manager: ClientManager,
|
||||
state: State,
|
||||
}
|
||||
|
||||
const UDP_RX: Token = Token(0);
|
||||
const FRONTEND_RX: Token = Token(1);
|
||||
const PRODUCER_RX: Token = Token(2);
|
||||
#[cfg(not(windows))]
|
||||
const SIGNAL: Token = Token(3);
|
||||
|
||||
impl Server {
|
||||
pub fn new(
|
||||
port: u16,
|
||||
mut producer: Box<dyn EventProducer>,
|
||||
consumer: Box<dyn EventConsumer>,
|
||||
mut frontend: FrontendAdapter,
|
||||
) -> Result<Self> {
|
||||
// bind the udp socket
|
||||
let listen_addr = SocketAddr::new("0.0.0.0".parse().unwrap(), port);
|
||||
let mut socket = UdpSocket::bind(listen_addr)?;
|
||||
|
||||
// register event sources
|
||||
let poll = Poll::new()?;
|
||||
|
||||
// hand signal handling over to the event loop
|
||||
#[cfg(not(windows))]
|
||||
let mut signals = Signals::new(SignalSet::all())?;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
poll.registry().register(&mut signals, SIGNAL, Interest::READABLE)?;
|
||||
poll.registry().register(&mut socket, UDP_RX, Interest::READABLE | Interest::WRITABLE)?;
|
||||
poll.registry().register(&mut producer, PRODUCER_RX, Interest::READABLE)?;
|
||||
poll.registry().register(&mut frontend, FRONTEND_RX, Interest::READABLE)?;
|
||||
|
||||
// create client manager
|
||||
let client_manager = ClientManager::new();
|
||||
Ok(Server {
|
||||
poll, socket, consumer, producer,
|
||||
#[cfg(not(windows))]
|
||||
signals, frontend,
|
||||
client_manager,
|
||||
state: State::Receiving,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run(&mut self) -> Result<()> {
|
||||
let mut events = Events::with_capacity(10);
|
||||
loop {
|
||||
match self.poll.poll(&mut events, None) {
|
||||
Ok(()) => (),
|
||||
Err(e) if e.kind() == ErrorKind::Interrupted => continue,
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
for event in &events {
|
||||
if !event.is_readable() { continue }
|
||||
match event.token() {
|
||||
UDP_RX => self.handle_udp_rx(),
|
||||
PRODUCER_RX => self.handle_producer_rx(),
|
||||
FRONTEND_RX => if self.handle_frontend_rx() { return Ok(()) },
|
||||
#[cfg(not(windows))]
|
||||
SIGNAL => if self.handle_signal() { return Ok(()) },
|
||||
_ => panic!("what happened here?")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_client(&mut self, addr: HashSet<SocketAddr>, pos: Position) {
|
||||
let client = self.client_manager.add_client(addr, pos);
|
||||
self.producer.notify(ClientEvent::Create(client, pos));
|
||||
self.consumer.notify(ClientEvent::Create(client, pos));
|
||||
}
|
||||
|
||||
fn handle_udp_rx(&mut self) {
|
||||
loop {
|
||||
let (event, addr) = match self.receive_event() {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
if e.is::<std::io::Error>() {
|
||||
if let ErrorKind::WouldBlock = e.downcast_ref::<std::io::Error>()
|
||||
.unwrap()
|
||||
.kind() {
|
||||
return
|
||||
}
|
||||
}
|
||||
log::error!("{}", e);
|
||||
continue
|
||||
}
|
||||
};
|
||||
log::trace!("{:20} <-<-<-<------ {addr}", event.to_string());
|
||||
|
||||
// get handle for addr
|
||||
let handle = match self.client_manager.get_client(addr) {
|
||||
Some(a) => a,
|
||||
None => {
|
||||
log::warn!("ignoring event from client {addr:?}");
|
||||
continue
|
||||
}
|
||||
};
|
||||
|
||||
// reset ttl for client and set addr as new default for this client
|
||||
self.client_manager.reset_last_seen(handle);
|
||||
self.client_manager.set_default_addr(handle, addr);
|
||||
match (event, addr) {
|
||||
(Event::Pong(), _) => {},
|
||||
(Event::Ping(), addr) => {
|
||||
if let Err(e) = Self::send_event(&self.socket, Event::Pong(), addr) {
|
||||
log::error!("udp send: {}", e);
|
||||
}
|
||||
}
|
||||
(event, addr) => {
|
||||
match self.state {
|
||||
State::Sending => {
|
||||
// in sending state, we dont want to process
|
||||
// any events to avoid feedback loops,
|
||||
// therefore we tell the event producer
|
||||
// to release the pointer and move on
|
||||
// first event -> release pointer
|
||||
if let Event::Release() = event {
|
||||
log::debug!("releasing pointer ...");
|
||||
self.producer.release();
|
||||
self.state = State::Receiving;
|
||||
}
|
||||
}
|
||||
State::Receiving => {
|
||||
// consume event
|
||||
self.consumer.consume(event, handle);
|
||||
|
||||
// let the server know we are still alive once every second
|
||||
let last_replied = self.client_manager.last_replied(handle);
|
||||
if last_replied.is_none()
|
||||
|| last_replied.is_some() && last_replied.unwrap() > Duration::from_secs(1) {
|
||||
self.client_manager.reset_last_replied(handle);
|
||||
if let Err(e) = Self::send_event(&self.socket, Event::Pong(), addr) {
|
||||
log::error!("udp send: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_producer_rx(&mut self) {
|
||||
let events = self.producer.read_events();
|
||||
for (c, e) in events.into_iter() {
|
||||
// in receiving state, only release events
|
||||
// must be transmitted
|
||||
if let Event::Release() = e {
|
||||
self.state = State::Sending;
|
||||
}
|
||||
// otherwise we should have an address to send to
|
||||
// transmit events to the corrensponding client
|
||||
if let Some(addr) = self.client_manager.get_active_addr(c) {
|
||||
log::trace!("{:20} ------>->->-> {addr}", e.to_string());
|
||||
if let Err(e) = Self::send_event(&self.socket, e, addr) {
|
||||
log::error!("udp send: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// if client last responded > 2 seconds ago
|
||||
// and we have not sent a ping since 500 milliseconds,
|
||||
// send a ping
|
||||
let last_seen = self.client_manager.last_seen(c);
|
||||
let last_ping = self.client_manager.last_ping(c);
|
||||
if last_seen.is_some() && last_seen.unwrap() < Duration::from_secs(2) {
|
||||
continue
|
||||
}
|
||||
|
||||
// client last seen > 500ms ago
|
||||
if last_ping.is_some() && last_ping.unwrap() < Duration::from_millis(500) {
|
||||
continue
|
||||
}
|
||||
|
||||
// last ping > 500ms ago -> ping all interfaces
|
||||
self.client_manager.reset_last_ping(c);
|
||||
if let Some(iter) = self.client_manager.get_addrs(c) {
|
||||
for addr in iter {
|
||||
log::debug!("pinging {addr}");
|
||||
if let Err(e) = Self::send_event(&self.socket, Event::Ping(), addr) {
|
||||
if e.kind() != ErrorKind::WouldBlock {
|
||||
log::error!("udp send: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// TODO should repeat dns lookup
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_frontend_rx(&mut self) -> bool {
|
||||
loop {
|
||||
match self.frontend.read_event() {
|
||||
Ok(event) => match event {
|
||||
FrontendEvent::RequestPortChange(_) => todo!(),
|
||||
FrontendEvent::RequestClientAdd(addr, pos) => {
|
||||
self.add_client(HashSet::from_iter(&mut [addr].into_iter()), pos);
|
||||
}
|
||||
FrontendEvent::RequestClientDelete(_) => todo!(),
|
||||
FrontendEvent::RequestClientUpdate(_) => todo!(),
|
||||
FrontendEvent::RequestShutdown() => {
|
||||
log::info!("terminating gracefully...");
|
||||
return true;
|
||||
},
|
||||
}
|
||||
Err(e) if e.kind() == ErrorKind::WouldBlock => return false,
|
||||
Err(e) => {
|
||||
log::error!("frontend: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn handle_signal(&mut self) -> bool {
|
||||
#[cfg(windows)]
|
||||
return false;
|
||||
#[cfg(not(windows))]
|
||||
loop {
|
||||
match self.signals.receive() {
|
||||
Err(e) if e.kind() == ErrorKind::WouldBlock => return false,
|
||||
Err(e) => {
|
||||
log::error!("error reading signal: {e}");
|
||||
return false;
|
||||
}
|
||||
Ok(Some(Signal::Interrupt) | Some(Signal::Terminate)) => {
|
||||
// terminate on SIG_INT or SIG_TERM
|
||||
log::info!("terminating gracefully...");
|
||||
return true;
|
||||
},
|
||||
Ok(Some(signal)) => {
|
||||
log::info!("ignoring signal {signal:?}");
|
||||
},
|
||||
Ok(None) => return false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_event(sock: &UdpSocket, e: Event, addr: SocketAddr) -> Result<usize> {
|
||||
let data: Vec<u8> = (&e).into();
|
||||
// We are currently abusing a blocking send to get the lowest possible latency.
|
||||
// It may be better to set the socket to non-blocking and only send when ready.
|
||||
sock.send_to(&data[..], addr)
|
||||
}
|
||||
|
||||
fn receive_event(&self) -> std::result::Result<(Event, SocketAddr), Box<dyn Error>> {
|
||||
let mut buf = vec![0u8; 22];
|
||||
match self.socket.recv_from(&mut buf) {
|
||||
Ok((_amt, src)) => Ok((Event::try_from(buf)?, src)),
|
||||
Err(e) => Err(Box::new(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
270
src/frontend.rs
270
src/frontend.rs
@@ -1,115 +1,271 @@
|
||||
use std::io::{Read, Result};
|
||||
use std::{str, net::SocketAddr};
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::{cmp::min, io::ErrorKind, str, time::Duration};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::{env, path::{Path, PathBuf}};
|
||||
use std::{
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use mio::{Registry, Token, event::Source};
|
||||
use tokio::io::ReadHalf;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, WriteHalf};
|
||||
|
||||
#[cfg(unix)]
|
||||
use mio::net::UnixListener;
|
||||
use tokio::net::UnixListener;
|
||||
#[cfg(unix)]
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
#[cfg(windows)]
|
||||
use mio::net::TcpListener;
|
||||
use tokio::net::TcpListener;
|
||||
#[cfg(windows)]
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::client::{Client, Position};
|
||||
use crate::{
|
||||
client::{Client, ClientHandle, Position},
|
||||
config::{Config, Frontend},
|
||||
};
|
||||
|
||||
/// cli frontend
|
||||
pub mod cli;
|
||||
|
||||
/// gtk frontend
|
||||
#[cfg(all(unix, feature = "gtk"))]
|
||||
#[cfg(feature = "gtk")]
|
||||
pub mod gtk;
|
||||
|
||||
pub fn run_frontend(config: &Config) -> Result<()> {
|
||||
match config.frontend {
|
||||
#[cfg(feature = "gtk")]
|
||||
Frontend::Gtk => {
|
||||
gtk::run();
|
||||
}
|
||||
#[cfg(not(feature = "gtk"))]
|
||||
Frontend::Gtk => panic!("gtk frontend requested but feature not enabled!"),
|
||||
Frontend::Cli => {
|
||||
cli::run()?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn exponential_back_off(duration: &mut Duration) -> &Duration {
|
||||
let new = duration.saturating_mul(2);
|
||||
*duration = min(new, Duration::from_secs(1));
|
||||
duration
|
||||
}
|
||||
|
||||
/// wait for the lan-mouse socket to come online
|
||||
#[cfg(unix)]
|
||||
pub fn wait_for_service() -> Result<std::os::unix::net::UnixStream> {
|
||||
let socket_path = FrontendListener::socket_path()?;
|
||||
let mut duration = Duration::from_millis(1);
|
||||
loop {
|
||||
use std::os::unix::net::UnixStream;
|
||||
if let Ok(stream) = UnixStream::connect(&socket_path) {
|
||||
break Ok(stream);
|
||||
}
|
||||
// a signaling mechanism or inotify could be used to
|
||||
// improve this
|
||||
std::thread::sleep(*exponential_back_off(&mut duration));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn wait_for_service() -> Result<std::net::TcpStream> {
|
||||
let mut duration = Duration::from_millis(1);
|
||||
loop {
|
||||
use std::net::TcpStream;
|
||||
if let Ok(stream) = TcpStream::connect("127.0.0.1:5252") {
|
||||
break Ok(stream);
|
||||
}
|
||||
std::thread::sleep(*exponential_back_off(&mut duration));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub enum FrontendEvent {
|
||||
RequestPortChange(u16),
|
||||
RequestClientAdd(SocketAddr, Position),
|
||||
RequestClientDelete(Client),
|
||||
RequestClientUpdate(Client),
|
||||
RequestShutdown(),
|
||||
/// add a new client
|
||||
AddClient(Option<String>, u16, Position),
|
||||
/// activate/deactivate client
|
||||
ActivateClient(ClientHandle, bool),
|
||||
/// change the listen port (recreate udp listener)
|
||||
ChangePort(u16),
|
||||
/// remove a client
|
||||
DelClient(ClientHandle),
|
||||
/// request an enumertaion of all clients
|
||||
Enumerate(),
|
||||
/// service shutdown
|
||||
Shutdown(),
|
||||
/// update a client (hostname, port, position)
|
||||
UpdateClient(ClientHandle, Option<String>, u16, Position),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum FrontendNotify {
|
||||
NotifyClientCreate(Client),
|
||||
NotifyClientCreate(ClientHandle, Option<String>, u16, Position),
|
||||
NotifyClientUpdate(ClientHandle, Option<String>, u16, Position),
|
||||
NotifyClientDelete(ClientHandle),
|
||||
/// new port, reason of failure (if failed)
|
||||
NotifyPortChange(u16, Option<String>),
|
||||
Enumerate(Vec<(Client, bool)>),
|
||||
NotifyError(String),
|
||||
}
|
||||
|
||||
pub struct FrontendAdapter {
|
||||
pub struct FrontendListener {
|
||||
#[cfg(windows)]
|
||||
listener: TcpListener,
|
||||
#[cfg(unix)]
|
||||
listener: UnixListener,
|
||||
#[cfg(unix)]
|
||||
socket_path: PathBuf,
|
||||
#[cfg(unix)]
|
||||
tx_streams: Vec<WriteHalf<UnixStream>>,
|
||||
#[cfg(windows)]
|
||||
tx_streams: Vec<WriteHalf<TcpStream>>,
|
||||
}
|
||||
|
||||
impl FrontendAdapter {
|
||||
pub fn new() -> std::result::Result<Self, Box<dyn std::error::Error>> {
|
||||
impl FrontendListener {
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
pub fn socket_path() -> Result<PathBuf> {
|
||||
let xdg_runtime_dir = match env::var("XDG_RUNTIME_DIR") {
|
||||
Ok(d) => d,
|
||||
Err(e) => return Err(anyhow!("could not find XDG_RUNTIME_DIR: {e}")),
|
||||
};
|
||||
let xdg_runtime_dir = Path::new(xdg_runtime_dir.as_str());
|
||||
Ok(xdg_runtime_dir.join("lan-mouse-socket.sock"))
|
||||
}
|
||||
|
||||
#[cfg(all(unix, target_os = "macos"))]
|
||||
pub fn socket_path() -> Result<PathBuf> {
|
||||
let home = match env::var("HOME") {
|
||||
Ok(d) => d,
|
||||
Err(e) => return Err(anyhow!("could not find HOME: {e}")),
|
||||
};
|
||||
let home = Path::new(home.as_str());
|
||||
let path = home
|
||||
.join("Library")
|
||||
.join("Caches")
|
||||
.join("lan-mouse-socket.sock");
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
pub async fn new() -> Option<Result<Self>> {
|
||||
#[cfg(unix)]
|
||||
let socket_path = Path::new(env::var("XDG_RUNTIME_DIR")?.as_str()).join("lan-mouse-socket.sock");
|
||||
#[cfg(unix)]
|
||||
let listener = UnixListener::bind(&socket_path)?;
|
||||
let (socket_path, listener) = {
|
||||
let socket_path = match Self::socket_path() {
|
||||
Ok(path) => path,
|
||||
Err(e) => return Some(Err(e)),
|
||||
};
|
||||
|
||||
log::debug!("remove socket: {:?}", socket_path);
|
||||
if socket_path.exists() {
|
||||
// try to connect to see if some other instance
|
||||
// of lan-mouse is already running
|
||||
match UnixStream::connect(&socket_path).await {
|
||||
// connected -> lan-mouse is already running
|
||||
Ok(_) => return None,
|
||||
// lan-mouse is not running but a socket was left behind
|
||||
Err(e) => {
|
||||
log::debug!("{socket_path:?}: {e} - removing left behind socket");
|
||||
let _ = std::fs::remove_file(&socket_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
let listener = match UnixListener::bind(&socket_path) {
|
||||
Ok(ls) => ls,
|
||||
// some other lan-mouse instance has bound the socket in the meantime
|
||||
Err(e) if e.kind() == ErrorKind::AddrInUse => return None,
|
||||
Err(e) => return Some(Err(anyhow!("failed to bind lan-mouse-socket: {e}"))),
|
||||
};
|
||||
(socket_path, listener)
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
let listener = TcpListener::bind("127.0.0.1:5252".parse().unwrap())?; // abuse tcp
|
||||
let listener = match TcpListener::bind("127.0.0.1:5252").await {
|
||||
Ok(ls) => ls,
|
||||
// some other lan-mouse instance has bound the socket in the meantime
|
||||
Err(e) if e.kind() == ErrorKind::AddrInUse => return None,
|
||||
Err(e) => return Some(Err(anyhow!("failed to bind lan-mouse-socket: {e}"))),
|
||||
};
|
||||
|
||||
let adapter = Self {
|
||||
listener,
|
||||
#[cfg(unix)]
|
||||
socket_path,
|
||||
tx_streams: vec![],
|
||||
};
|
||||
|
||||
Ok(adapter)
|
||||
Some(Ok(adapter))
|
||||
}
|
||||
|
||||
pub fn read_event(&mut self) -> Result<FrontendEvent>{
|
||||
let (mut stream, _) = self.listener.accept()?;
|
||||
let mut buf = [0u8; 128]; // FIXME
|
||||
stream.read(&mut buf)?;
|
||||
let json = str::from_utf8(&buf)
|
||||
.unwrap()
|
||||
.trim_end_matches(char::from(0)); // remove trailing 0-bytes
|
||||
let event = serde_json::from_str(json).unwrap();
|
||||
log::debug!("{:?}", event);
|
||||
Ok(event)
|
||||
#[cfg(unix)]
|
||||
pub async fn accept(&mut self) -> Result<ReadHalf<UnixStream>> {
|
||||
let stream = self.listener.accept().await?.0;
|
||||
let (rx, tx) = tokio::io::split(stream);
|
||||
self.tx_streams.push(tx);
|
||||
Ok(rx)
|
||||
}
|
||||
|
||||
pub fn notify(&self, _event: FrontendNotify) { }
|
||||
}
|
||||
|
||||
impl Source for FrontendAdapter {
|
||||
fn register(
|
||||
&mut self,
|
||||
registry: &Registry,
|
||||
token: Token,
|
||||
interests: mio::Interest,
|
||||
) -> Result<()> {
|
||||
self.listener.register(registry, token, interests)
|
||||
#[cfg(windows)]
|
||||
pub async fn accept(&mut self) -> Result<ReadHalf<TcpStream>> {
|
||||
let stream = self.listener.accept().await?.0;
|
||||
let (rx, tx) = tokio::io::split(stream);
|
||||
self.tx_streams.push(tx);
|
||||
Ok(rx)
|
||||
}
|
||||
|
||||
fn reregister(
|
||||
&mut self,
|
||||
registry: &Registry,
|
||||
token: Token,
|
||||
interests: mio::Interest,
|
||||
) -> Result<()> {
|
||||
self.listener.reregister(registry, token, interests)
|
||||
}
|
||||
pub(crate) async fn notify_all(&mut self, notify: FrontendNotify) -> Result<()> {
|
||||
// encode event
|
||||
let json = serde_json::to_string(¬ify).unwrap();
|
||||
let payload = json.as_bytes();
|
||||
let len = payload.len().to_be_bytes();
|
||||
log::debug!("json: {json}, len: {}", payload.len());
|
||||
|
||||
fn deregister(&mut self, registry: &Registry) -> Result<()> {
|
||||
self.listener.deregister(registry)
|
||||
let mut keep = vec![];
|
||||
|
||||
// TODO do simultaneously
|
||||
for tx in self.tx_streams.iter_mut() {
|
||||
// write len + payload
|
||||
if tx.write(&len).await.is_err() {
|
||||
keep.push(false);
|
||||
continue;
|
||||
}
|
||||
if tx.write(payload).await.is_err() {
|
||||
keep.push(false);
|
||||
continue;
|
||||
}
|
||||
keep.push(true);
|
||||
}
|
||||
|
||||
// could not find a better solution because async
|
||||
let mut keep = keep.into_iter();
|
||||
self.tx_streams.retain(|_| keep.next().unwrap());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl Drop for FrontendAdapter {
|
||||
impl Drop for FrontendListener {
|
||||
fn drop(&mut self) {
|
||||
log::debug!("remove socket: {:?}", self.socket_path);
|
||||
std::fs::remove_file(&self.socket_path).unwrap();
|
||||
let _ = std::fs::remove_file(&self.socket_path);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Frontend { }
|
||||
#[cfg(unix)]
|
||||
pub async fn read_event(stream: &mut ReadHalf<UnixStream>) -> Result<FrontendEvent> {
|
||||
let len = stream.read_u64().await?;
|
||||
assert!(len <= 256);
|
||||
let mut buf = [0u8; 256];
|
||||
stream.read_exact(&mut buf[..len as usize]).await?;
|
||||
Ok(serde_json::from_slice(&buf[..len as usize])?)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub async fn read_event(stream: &mut ReadHalf<TcpStream>) -> Result<FrontendEvent> {
|
||||
let len = stream.read_u64().await?;
|
||||
let mut buf = [0u8; 256];
|
||||
stream.read_exact(&mut buf[..len as usize]).await?;
|
||||
Ok(serde_json::from_slice(&buf[..len as usize])?)
|
||||
}
|
||||
|
||||
@@ -1,85 +1,233 @@
|
||||
use anyhow::Result;
|
||||
use std::{thread, io::Write, net::SocketAddr};
|
||||
#[cfg(windows)]
|
||||
use std::net::SocketAddrV4;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::{os::unix::net::UnixStream, path::Path, env};
|
||||
#[cfg(windows)]
|
||||
use std::net::TcpStream;
|
||||
use std::{
|
||||
io::{ErrorKind, Read, Write},
|
||||
str::SplitWhitespace,
|
||||
thread,
|
||||
};
|
||||
|
||||
use crate::client::Position;
|
||||
use crate::{client::Position, config::DEFAULT_PORT};
|
||||
|
||||
use super::{FrontendEvent, Frontend};
|
||||
use super::{FrontendEvent, FrontendNotify};
|
||||
|
||||
pub struct CliFrontend;
|
||||
pub fn run() -> Result<()> {
|
||||
let Ok(mut tx) = super::wait_for_service() else {
|
||||
return Err(anyhow!("Could not connect to lan-mouse-socket"));
|
||||
};
|
||||
|
||||
impl Frontend for CliFrontend {}
|
||||
let mut rx = tx.try_clone()?;
|
||||
|
||||
impl CliFrontend {
|
||||
pub fn new() -> Result<CliFrontend> {
|
||||
#[cfg(unix)]
|
||||
let socket_path = Path::new(env::var("XDG_RUNTIME_DIR")?.as_str()).join("lan-mouse-socket.sock");
|
||||
thread::Builder::new()
|
||||
.name("cli-frontend".to_string())
|
||||
.spawn(move || {
|
||||
let reader = thread::Builder::new()
|
||||
.name("cli-frontend".to_string())
|
||||
.spawn(move || {
|
||||
// all further prompts
|
||||
prompt();
|
||||
loop {
|
||||
eprint!("lan-mouse > ");
|
||||
std::io::stderr().flush().unwrap();
|
||||
let mut buf = String::new();
|
||||
match std::io::stdin().read_line(&mut buf) {
|
||||
Ok(0) => return,
|
||||
Ok(len) => {
|
||||
if let Some(event) = parse_event(buf, len) {
|
||||
#[cfg(unix)]
|
||||
let Ok(mut stream) = UnixStream::connect(&socket_path) else {
|
||||
log::error!("Could not connect to lan-mouse-socket");
|
||||
continue;
|
||||
};
|
||||
#[cfg(windows)]
|
||||
let Ok(mut stream) = TcpStream::connect("127.0.0.1:5252".parse::<SocketAddrV4>().unwrap()) else {
|
||||
log::error!("Could not connect to lan-mouse-server");
|
||||
continue;
|
||||
};
|
||||
let json = serde_json::to_string(&event).unwrap();
|
||||
if let Err(e) = stream.write(json.as_bytes()) {
|
||||
log::error!("error sending message: {e}");
|
||||
};
|
||||
if event == FrontendEvent::RequestShutdown() {
|
||||
break;
|
||||
if let Some(events) = parse_cmd(buf, len) {
|
||||
for event in events.iter() {
|
||||
let json = serde_json::to_string(&event).unwrap();
|
||||
let bytes = json.as_bytes();
|
||||
let len = bytes.len().to_be_bytes();
|
||||
if let Err(e) = tx.write(&len) {
|
||||
log::error!("error sending message: {e}");
|
||||
};
|
||||
if let Err(e) = tx.write(bytes) {
|
||||
log::error!("error sending message: {e}");
|
||||
};
|
||||
if *event == FrontendEvent::Shutdown() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// prompt is printed after the server response is received
|
||||
} else {
|
||||
prompt();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("{e:?}");
|
||||
break
|
||||
if e.kind() != ErrorKind::UnexpectedEof {
|
||||
log::error!("error reading from stdin: {e}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}).unwrap();
|
||||
Ok(Self {})
|
||||
})?;
|
||||
|
||||
let _ = thread::Builder::new()
|
||||
.name("cli-frontend-notify".to_string())
|
||||
.spawn(move || {
|
||||
loop {
|
||||
// read len
|
||||
let mut len = [0u8; 8];
|
||||
match rx.read_exact(&mut len) {
|
||||
Ok(()) => (),
|
||||
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break,
|
||||
Err(e) => break log::error!("{e}"),
|
||||
};
|
||||
let len = usize::from_be_bytes(len);
|
||||
|
||||
// read payload
|
||||
let mut buf: Vec<u8> = vec![0u8; len];
|
||||
match rx.read_exact(&mut buf[..len]) {
|
||||
Ok(()) => (),
|
||||
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break,
|
||||
Err(e) => break log::error!("{e}"),
|
||||
};
|
||||
|
||||
let notify: FrontendNotify = match serde_json::from_slice(&buf) {
|
||||
Ok(n) => n,
|
||||
Err(e) => break log::error!("{e}"),
|
||||
};
|
||||
match notify {
|
||||
FrontendNotify::NotifyClientCreate(client, host, port, pos) => {
|
||||
log::info!(
|
||||
"new client ({client}): {}:{port} - {pos}",
|
||||
host.as_deref().unwrap_or("")
|
||||
);
|
||||
}
|
||||
FrontendNotify::NotifyClientUpdate(client, host, port, pos) => {
|
||||
log::info!(
|
||||
"client ({client}) updated: {}:{port} - {pos}",
|
||||
host.as_deref().unwrap_or("")
|
||||
);
|
||||
}
|
||||
FrontendNotify::NotifyClientDelete(client) => {
|
||||
log::info!("client ({client}) deleted.");
|
||||
}
|
||||
FrontendNotify::NotifyError(e) => {
|
||||
log::warn!("{e}");
|
||||
}
|
||||
FrontendNotify::Enumerate(clients) => {
|
||||
for (client, active) in clients.into_iter() {
|
||||
log::info!(
|
||||
"client ({}) [{}]: active: {}, associated addresses: [{}]",
|
||||
client.handle,
|
||||
client.hostname.as_deref().unwrap_or(""),
|
||||
if active { "yes" } else { "no" },
|
||||
client
|
||||
.addrs
|
||||
.into_iter()
|
||||
.map(|a| a.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
);
|
||||
}
|
||||
}
|
||||
FrontendNotify::NotifyPortChange(port, msg) => match msg {
|
||||
Some(msg) => log::info!("could not change port: {msg}"),
|
||||
None => log::info!("port changed: {port}"),
|
||||
},
|
||||
}
|
||||
prompt();
|
||||
}
|
||||
})?;
|
||||
match reader.join() {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
let msg = match (e.downcast_ref::<&str>(), e.downcast_ref::<String>()) {
|
||||
(Some(&s), _) => s,
|
||||
(_, Some(s)) => s,
|
||||
_ => "no panic info",
|
||||
};
|
||||
log::error!("reader thread paniced: {msg}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_event(s: String, len: usize) -> Option<FrontendEvent> {
|
||||
fn prompt() {
|
||||
eprint!("lan-mouse > ");
|
||||
std::io::stderr().flush().unwrap();
|
||||
}
|
||||
|
||||
fn parse_cmd(s: String, len: usize) -> Option<Vec<FrontendEvent>> {
|
||||
if len == 0 {
|
||||
return Some(FrontendEvent::RequestShutdown())
|
||||
return Some(vec![FrontendEvent::Shutdown()]);
|
||||
}
|
||||
let mut l = s.split_whitespace();
|
||||
let cmd = l.next()?;
|
||||
match cmd {
|
||||
"connect" => {
|
||||
let addr = match l.next()?.parse() {
|
||||
Ok(addr) => SocketAddr::V4(addr),
|
||||
Err(e) => {
|
||||
log::error!("parse error: {e}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
Some(FrontendEvent::RequestClientAdd(addr, Position::Left ))
|
||||
let res = match cmd {
|
||||
"help" => {
|
||||
log::info!("list list clients");
|
||||
log::info!("connect <host> left|right|top|bottom [port] add a new client");
|
||||
log::info!("disconnect <client> remove a client");
|
||||
log::info!("activate <client> activate a client");
|
||||
log::info!("deactivate <client> deactivate a client");
|
||||
log::info!("exit exit lan-mouse");
|
||||
log::info!("setport <port> change port");
|
||||
None
|
||||
}
|
||||
"exit" => return Some(vec![FrontendEvent::Shutdown()]),
|
||||
"list" => return Some(vec![FrontendEvent::Enumerate()]),
|
||||
"connect" => Some(parse_connect(l)),
|
||||
"disconnect" => Some(parse_disconnect(l)),
|
||||
"activate" => Some(parse_activate(l)),
|
||||
"deactivate" => Some(parse_deactivate(l)),
|
||||
"setport" => Some(parse_port(l)),
|
||||
_ => {
|
||||
log::error!("unknown command: {s}");
|
||||
None
|
||||
}
|
||||
};
|
||||
match res {
|
||||
Some(Ok(e)) => Some(e),
|
||||
Some(Err(e)) => {
|
||||
log::warn!("{e}");
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_connect(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> {
|
||||
let usage = "usage: connect <host> left|right|top|bottom [port]";
|
||||
let host = l.next().context(usage)?.to_owned();
|
||||
let pos = match l.next().context(usage)? {
|
||||
"right" => Position::Right,
|
||||
"top" => Position::Top,
|
||||
"bottom" => Position::Bottom,
|
||||
_ => Position::Left,
|
||||
};
|
||||
let port = if let Some(p) = l.next() {
|
||||
p.parse()?
|
||||
} else {
|
||||
DEFAULT_PORT
|
||||
};
|
||||
Ok(vec![
|
||||
FrontendEvent::AddClient(Some(host), port, pos),
|
||||
FrontendEvent::Enumerate(),
|
||||
])
|
||||
}
|
||||
|
||||
fn parse_disconnect(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> {
|
||||
let client = l.next().context("usage: disconnect <client_id>")?.parse()?;
|
||||
Ok(vec![
|
||||
FrontendEvent::DelClient(client),
|
||||
FrontendEvent::Enumerate(),
|
||||
])
|
||||
}
|
||||
|
||||
fn parse_activate(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> {
|
||||
let client = l.next().context("usage: activate <client_id>")?.parse()?;
|
||||
Ok(vec![
|
||||
FrontendEvent::ActivateClient(client, true),
|
||||
FrontendEvent::Enumerate(),
|
||||
])
|
||||
}
|
||||
|
||||
fn parse_deactivate(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> {
|
||||
let client = l.next().context("usage: deactivate <client_id>")?.parse()?;
|
||||
Ok(vec![
|
||||
FrontendEvent::ActivateClient(client, false),
|
||||
FrontendEvent::Enumerate(),
|
||||
])
|
||||
}
|
||||
|
||||
fn parse_port(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> {
|
||||
let port = l.next().context("usage: setport <port>")?.parse()?;
|
||||
Ok(vec![FrontendEvent::ChangePort(port)])
|
||||
}
|
||||
|
||||
208
src/frontend/gtk.rs
Normal file
208
src/frontend/gtk.rs
Normal file
@@ -0,0 +1,208 @@
|
||||
mod client_object;
|
||||
mod client_row;
|
||||
mod window;
|
||||
|
||||
use std::{
|
||||
env,
|
||||
io::{ErrorKind, Read},
|
||||
process, str,
|
||||
};
|
||||
|
||||
use crate::{config::DEFAULT_PORT, frontend::gtk::window::Window};
|
||||
|
||||
use adw::Application;
|
||||
use gtk::{
|
||||
gdk::Display,
|
||||
gio::{SimpleAction, SimpleActionGroup},
|
||||
glib::clone,
|
||||
prelude::*,
|
||||
subclass::prelude::ObjectSubclassIsExt,
|
||||
CssProvider, IconTheme,
|
||||
};
|
||||
use gtk::{gio, glib, prelude::ApplicationExt};
|
||||
|
||||
use self::client_object::ClientObject;
|
||||
|
||||
use super::FrontendNotify;
|
||||
|
||||
pub fn run() -> glib::ExitCode {
|
||||
log::debug!("running gtk frontend");
|
||||
#[cfg(windows)]
|
||||
let ret = std::thread::Builder::new()
|
||||
.stack_size(8 * 1024 * 1024) // https://gitlab.gnome.org/GNOME/gtk/-/commit/52dbb3f372b2c3ea339e879689c1de535ba2c2c3 -> caused crash on windows
|
||||
.name("gtk".into())
|
||||
.spawn(gtk_main)
|
||||
.unwrap()
|
||||
.join()
|
||||
.unwrap();
|
||||
#[cfg(not(windows))]
|
||||
let ret = gtk_main();
|
||||
|
||||
log::debug!("frontend exited");
|
||||
ret
|
||||
}
|
||||
|
||||
fn gtk_main() -> glib::ExitCode {
|
||||
gio::resources_register_include!("lan-mouse.gresource").expect("Failed to register resources.");
|
||||
|
||||
let app = Application::builder()
|
||||
.application_id("de.feschber.lan-mouse")
|
||||
.build();
|
||||
|
||||
app.connect_startup(|_| load_icons());
|
||||
app.connect_startup(|_| load_css());
|
||||
app.connect_activate(build_ui);
|
||||
|
||||
let args: Vec<&'static str> = vec![];
|
||||
app.run_with_args(&args)
|
||||
}
|
||||
|
||||
fn load_css() {
|
||||
let provider = CssProvider::new();
|
||||
provider.load_from_resource("de/feschber/LanMouse/style.css");
|
||||
gtk::style_context_add_provider_for_display(
|
||||
&Display::default().expect("Could not connect to a display."),
|
||||
&provider,
|
||||
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
);
|
||||
}
|
||||
|
||||
fn load_icons() {
|
||||
let icon_theme =
|
||||
IconTheme::for_display(&Display::default().expect("Could not connect to a display."));
|
||||
icon_theme.add_resource_path("/de/feschber/LanMouse/icons");
|
||||
}
|
||||
|
||||
fn build_ui(app: &Application) {
|
||||
log::debug!("connecting to lan-mouse-socket");
|
||||
let mut rx = match super::wait_for_service() {
|
||||
Ok(stream) => stream,
|
||||
Err(e) => {
|
||||
log::error!("could not connect to lan-mouse-socket: {e}");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
let tx = match rx.try_clone() {
|
||||
Ok(sock) => sock,
|
||||
Err(e) => {
|
||||
log::error!("{e}");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
log::debug!("connected to lan-mouse-socket");
|
||||
|
||||
let (sender, receiver) = async_channel::bounded(10);
|
||||
|
||||
gio::spawn_blocking(move || {
|
||||
match loop {
|
||||
// read length
|
||||
let mut len = [0u8; 8];
|
||||
match rx.read_exact(&mut len) {
|
||||
Ok(_) => (),
|
||||
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()),
|
||||
Err(e) => break Err(e),
|
||||
};
|
||||
let len = usize::from_be_bytes(len);
|
||||
|
||||
// read payload
|
||||
let mut buf = vec![0u8; len];
|
||||
match rx.read_exact(&mut buf) {
|
||||
Ok(_) => (),
|
||||
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()),
|
||||
Err(e) => break Err(e),
|
||||
};
|
||||
|
||||
// parse json
|
||||
let json = str::from_utf8(&buf).unwrap();
|
||||
match serde_json::from_str(json) {
|
||||
Ok(notify) => sender.send_blocking(notify).unwrap(),
|
||||
Err(e) => log::error!("{e}"),
|
||||
}
|
||||
} {
|
||||
Ok(()) => {}
|
||||
Err(e) => log::error!("{e}"),
|
||||
}
|
||||
});
|
||||
|
||||
let window = Window::new(app);
|
||||
window.imp().stream.borrow_mut().replace(tx);
|
||||
glib::spawn_future_local(clone!(@weak window => async move {
|
||||
loop {
|
||||
let notify = receiver.recv().await.unwrap();
|
||||
match notify {
|
||||
FrontendNotify::NotifyClientCreate(client, hostname, port, position) => {
|
||||
window.new_client(client, hostname, port, position, false);
|
||||
},
|
||||
FrontendNotify::NotifyClientUpdate(client, hostname, port, position) => {
|
||||
log::info!("client updated: {client}, {}:{port}, {position}", hostname.unwrap_or("".to_string()));
|
||||
}
|
||||
FrontendNotify::NotifyError(e) => {
|
||||
// TODO
|
||||
log::error!("{e}");
|
||||
},
|
||||
FrontendNotify::NotifyClientDelete(client) => {
|
||||
window.delete_client(client);
|
||||
}
|
||||
FrontendNotify::Enumerate(clients) => {
|
||||
for (client, active) in clients {
|
||||
if window.client_idx(client.handle).is_some() {
|
||||
continue
|
||||
}
|
||||
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) => {
|
||||
match msg {
|
||||
None => window.show_toast(format!("port changed: {port}").as_str()),
|
||||
Some(msg) => window.show_toast(msg.as_str()),
|
||||
}
|
||||
window.imp().set_port(port);
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
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();
|
||||
}
|
||||
41
src/frontend/gtk/client_object.rs
Normal file
41
src/frontend/gtk/client_object.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
mod imp;
|
||||
|
||||
use adw::subclass::prelude::*;
|
||||
use gtk::glib::{self, Object};
|
||||
|
||||
use crate::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 {
|
||||
Object::builder()
|
||||
.property("handle", handle)
|
||||
.property("hostname", hostname)
|
||||
.property("port", port)
|
||||
.property("active", active)
|
||||
.property("position", position)
|
||||
.build()
|
||||
}
|
||||
|
||||
pub fn get_data(&self) -> ClientData {
|
||||
self.imp().data.borrow().clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct ClientData {
|
||||
pub handle: ClientHandle,
|
||||
pub hostname: Option<String>,
|
||||
pub port: u32,
|
||||
pub active: bool,
|
||||
pub position: String,
|
||||
}
|
||||
30
src/frontend/gtk/client_object/imp.rs
Normal file
30
src/frontend/gtk/client_object/imp.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use std::cell::RefCell;
|
||||
|
||||
use glib::Properties;
|
||||
use gtk::glib;
|
||||
use gtk::prelude::*;
|
||||
use gtk::subclass::prelude::*;
|
||||
|
||||
use crate::client::ClientHandle;
|
||||
|
||||
use super::ClientData;
|
||||
|
||||
#[derive(Properties, Default)]
|
||||
#[properties(wrapper_type = super::ClientObject)]
|
||||
pub struct ClientObject {
|
||||
#[property(name = "handle", get, set, type = ClientHandle, member = handle)]
|
||||
#[property(name = "hostname", get, set, type = String, member = hostname)]
|
||||
#[property(name = "port", get, set, type = u32, member = port, maximum = u16::MAX as u32)]
|
||||
#[property(name = "active", get, set, type = bool, member = active)]
|
||||
#[property(name = "position", get, set, type = String, member = position)]
|
||||
pub data: RefCell<ClientData>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for ClientObject {
|
||||
const NAME: &'static str = "ClientObject";
|
||||
type Type = super::ClientObject;
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for ClientObject {}
|
||||
119
src/frontend/gtk/client_row.rs
Normal file
119
src/frontend/gtk/client_row.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
mod imp;
|
||||
|
||||
use adw::prelude::*;
|
||||
use adw::subclass::prelude::*;
|
||||
use gtk::glib::{self, Object};
|
||||
|
||||
use crate::config::DEFAULT_PORT;
|
||||
|
||||
use super::ClientObject;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct ClientRow(ObjectSubclass<imp::ClientRow>)
|
||||
@extends gtk::ListBoxRow, gtk::Widget, adw::PreferencesRow, adw::ExpanderRow,
|
||||
@implements gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget;
|
||||
}
|
||||
|
||||
impl ClientRow {
|
||||
pub fn new(_client_object: &ClientObject) -> Self {
|
||||
Object::builder().build()
|
||||
}
|
||||
|
||||
pub fn bind(&self, client_object: &ClientObject) {
|
||||
let mut bindings = self.imp().bindings.borrow_mut();
|
||||
|
||||
let active_binding = client_object
|
||||
.bind_property("active", &self.imp().enable_switch.get(), "state")
|
||||
.bidirectional()
|
||||
.sync_create()
|
||||
.build();
|
||||
|
||||
let hostname_binding = client_object
|
||||
.bind_property("hostname", &self.imp().hostname.get(), "text")
|
||||
.transform_to(|_, v: Option<String>| {
|
||||
if let Some(hostname) = v {
|
||||
Some(hostname)
|
||||
} else {
|
||||
Some("".to_string())
|
||||
}
|
||||
})
|
||||
.transform_from(|_, v: String| {
|
||||
if v.as_str().trim() == "" {
|
||||
Some(None)
|
||||
} else {
|
||||
Some(Some(v))
|
||||
}
|
||||
})
|
||||
.bidirectional()
|
||||
.sync_create()
|
||||
.build();
|
||||
|
||||
let title_binding = client_object
|
||||
.bind_property("hostname", self, "title")
|
||||
.transform_to(|_, v: Option<String>| {
|
||||
if let Some(hostname) = v {
|
||||
Some(hostname)
|
||||
} else {
|
||||
Some("<span font_style=\"italic\" font_weight=\"light\" foreground=\"darkgrey\">no hostname!</span>".to_string())
|
||||
}
|
||||
})
|
||||
.sync_create()
|
||||
.build();
|
||||
|
||||
let port_binding = client_object
|
||||
.bind_property("port", &self.imp().port.get(), "text")
|
||||
.transform_from(|_, v: String| {
|
||||
if v.is_empty() {
|
||||
Some(DEFAULT_PORT as u32)
|
||||
} else {
|
||||
Some(v.parse::<u16>().unwrap_or(DEFAULT_PORT) as u32)
|
||||
}
|
||||
})
|
||||
.transform_to(|_, v: u32| {
|
||||
if v == 4242 {
|
||||
Some("".to_string())
|
||||
} else {
|
||||
Some(v.to_string())
|
||||
}
|
||||
})
|
||||
.bidirectional()
|
||||
.sync_create()
|
||||
.build();
|
||||
|
||||
let subtitle_binding = client_object
|
||||
.bind_property("port", self, "subtitle")
|
||||
.sync_create()
|
||||
.build();
|
||||
|
||||
let position_binding = client_object
|
||||
.bind_property("position", &self.imp().position.get(), "selected")
|
||||
.transform_from(|_, v: u32| match v {
|
||||
1 => Some("right"),
|
||||
2 => Some("top"),
|
||||
3 => Some("bottom"),
|
||||
_ => Some("left"),
|
||||
})
|
||||
.transform_to(|_, v: String| match v.as_str() {
|
||||
"right" => Some(1),
|
||||
"top" => Some(2u32),
|
||||
"bottom" => Some(3u32),
|
||||
_ => Some(0u32),
|
||||
})
|
||||
.bidirectional()
|
||||
.sync_create()
|
||||
.build();
|
||||
|
||||
bindings.push(active_binding);
|
||||
bindings.push(hostname_binding);
|
||||
bindings.push(title_binding);
|
||||
bindings.push(port_binding);
|
||||
bindings.push(subtitle_binding);
|
||||
bindings.push(position_binding);
|
||||
}
|
||||
|
||||
pub fn unbind(&self) {
|
||||
for binding in self.imp().bindings.borrow_mut().drain(..) {
|
||||
binding.unbind();
|
||||
}
|
||||
}
|
||||
}
|
||||
81
src/frontend/gtk/client_row/imp.rs
Normal file
81
src/frontend/gtk/client_row/imp.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use std::cell::RefCell;
|
||||
|
||||
use adw::subclass::prelude::*;
|
||||
use adw::{prelude::*, ActionRow, ComboRow};
|
||||
use glib::{subclass::InitializingObject, Binding};
|
||||
use gtk::glib::clone;
|
||||
use gtk::{glib, Button, CompositeTemplate, Switch};
|
||||
|
||||
#[derive(CompositeTemplate, Default)]
|
||||
#[template(resource = "/de/feschber/LanMouse/client_row.ui")]
|
||||
pub struct ClientRow {
|
||||
#[template_child]
|
||||
pub enable_switch: TemplateChild<gtk::Switch>,
|
||||
#[template_child]
|
||||
pub hostname: TemplateChild<gtk::Entry>,
|
||||
#[template_child]
|
||||
pub port: TemplateChild<gtk::Entry>,
|
||||
#[template_child]
|
||||
pub position: TemplateChild<ComboRow>,
|
||||
#[template_child]
|
||||
pub delete_row: TemplateChild<ActionRow>,
|
||||
#[template_child]
|
||||
pub delete_button: TemplateChild<gtk::Button>,
|
||||
pub bindings: RefCell<Vec<Binding>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for ClientRow {
|
||||
// `NAME` needs to match `class` attribute of template
|
||||
const NAME: &'static str = "ClientRow";
|
||||
type Type = super::ClientRow;
|
||||
type ParentType = adw::ExpanderRow;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.bind_template();
|
||||
klass.bind_template_callbacks();
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for ClientRow {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
self.delete_button
|
||||
.connect_clicked(clone!(@weak self as row => move |button| {
|
||||
row.handle_client_delete(button);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for ClientRow {}
|
||||
impl BoxImpl for ClientRow {}
|
||||
impl ListBoxRowImpl for ClientRow {}
|
||||
impl PreferencesRowImpl for ClientRow {}
|
||||
impl ExpanderRowImpl for ClientRow {}
|
||||
176
src/frontend/gtk/window.rs
Normal file
176
src/frontend/gtk/window.rs
Normal file
@@ -0,0 +1,176 @@
|
||||
mod imp;
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use adw::prelude::*;
|
||||
use adw::subclass::prelude::*;
|
||||
use glib::{clone, Object};
|
||||
use gtk::{gio, glib, NoSelection};
|
||||
|
||||
use crate::{
|
||||
client::{ClientHandle, Position},
|
||||
config::DEFAULT_PORT,
|
||||
frontend::{gtk::client_object::ClientObject, FrontendEvent},
|
||||
};
|
||||
|
||||
use super::client_row::ClientRow;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct Window(ObjectSubclass<imp::Window>)
|
||||
@extends adw::ApplicationWindow, gtk::Window, gtk::Widget,
|
||||
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
|
||||
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub(crate) fn new(app: &adw::Application) -> Self {
|
||||
Object::builder().property("application", app).build()
|
||||
}
|
||||
|
||||
pub fn clients(&self) -> gio::ListStore {
|
||||
self.imp()
|
||||
.clients
|
||||
.borrow()
|
||||
.clone()
|
||||
.expect("Could not get clients")
|
||||
}
|
||||
|
||||
fn setup_clients(&self) {
|
||||
let model = gio::ListStore::new::<ClientObject>();
|
||||
self.imp().clients.replace(Some(model));
|
||||
|
||||
let selection_model = NoSelection::new(Some(self.clients()));
|
||||
self.imp().client_list.bind_model(
|
||||
Some(&selection_model),
|
||||
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.upcast()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/// workaround for a bug in libadwaita that shows an ugly line beneath
|
||||
/// the last element if a placeholder is set.
|
||||
/// https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6308
|
||||
pub fn set_placeholder_visible(&self, visible: bool) {
|
||||
let placeholder = self.imp().client_placeholder.get();
|
||||
self.imp().client_list.set_placeholder(match visible {
|
||||
true => Some(&placeholder),
|
||||
false => None,
|
||||
});
|
||||
}
|
||||
|
||||
fn setup_icon(&self) {
|
||||
self.set_icon_name(Some("mouse-icon"));
|
||||
}
|
||||
|
||||
fn create_client_row(&self, client_object: &ClientObject) -> ClientRow {
|
||||
let row = ClientRow::new(client_object);
|
||||
row.bind(client_object);
|
||||
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);
|
||||
self.clients().append(&client);
|
||||
self.set_placeholder_visible(false);
|
||||
}
|
||||
|
||||
pub fn client_idx(&self, handle: ClientHandle) -> Option<usize> {
|
||||
self.clients().iter::<ClientObject>().position(|c| {
|
||||
if let Ok(c) = c {
|
||||
c.handle() == handle
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_client(&self, handle: ClientHandle) {
|
||||
let Some(idx) = self.client_idx(handle) else {
|
||||
log::warn!("could not find client with handle {handle}");
|
||||
return;
|
||||
};
|
||||
|
||||
self.clients().remove(idx as u32);
|
||||
if self.clients().n_items() == 0 {
|
||||
self.set_placeholder_visible(true);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_client_create(&self) {
|
||||
let event = FrontendEvent::AddClient(None, DEFAULT_PORT, Position::default());
|
||||
self.imp().set_port(DEFAULT_PORT);
|
||||
self.request(event);
|
||||
}
|
||||
|
||||
pub fn request_port_change(&self) {
|
||||
let port = self.imp().port_entry.get().text().to_string();
|
||||
if let Ok(port) = port.as_str().parse::<u16>() {
|
||||
self.request(FrontendEvent::ChangePort(port));
|
||||
} else {
|
||||
self.request(FrontendEvent::ChangePort(DEFAULT_PORT));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_client_update(&self, client: &ClientObject) {
|
||||
let data = client.get_data();
|
||||
let position = match data.position.as_str() {
|
||||
"left" => Position::Left,
|
||||
"right" => Position::Right,
|
||||
"top" => Position::Top,
|
||||
"bottom" => Position::Bottom,
|
||||
_ => {
|
||||
log::error!("invalid position: {}", data.position);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let hostname = data.hostname;
|
||||
let port = data.port as u16;
|
||||
let event = FrontendEvent::UpdateClient(client.handle(), hostname, port, position);
|
||||
self.request(event);
|
||||
|
||||
let event = FrontendEvent::ActivateClient(client.handle(), !client.active());
|
||||
self.request(event);
|
||||
}
|
||||
|
||||
pub fn request_client_delete(&self, idx: u32) {
|
||||
if let Some(obj) = self.clients().item(idx) {
|
||||
let client_object: &ClientObject = obj
|
||||
.downcast_ref()
|
||||
.expect("Expected object of type `ClientObject`.");
|
||||
let handle = client_object.handle();
|
||||
let event = FrontendEvent::DelClient(handle);
|
||||
self.request(event);
|
||||
}
|
||||
}
|
||||
|
||||
fn request(&self, event: FrontendEvent) {
|
||||
let json = serde_json::to_string(&event).unwrap();
|
||||
log::debug!("requesting {json}");
|
||||
let mut stream = self.imp().stream.borrow_mut();
|
||||
let stream = stream.as_mut().unwrap();
|
||||
let bytes = json.as_bytes();
|
||||
let len = bytes.len().to_be_bytes();
|
||||
if let Err(e) = stream.write(&len) {
|
||||
log::error!("error sending message: {e}");
|
||||
};
|
||||
if let Err(e) = stream.write(bytes) {
|
||||
log::error!("error sending message: {e}");
|
||||
};
|
||||
}
|
||||
|
||||
pub fn show_toast(&self, msg: &str) {
|
||||
let toast = adw::Toast::new(msg);
|
||||
let toast_overlay = &self.imp().toast_overlay;
|
||||
toast_overlay.add_toast(toast);
|
||||
}
|
||||
}
|
||||
110
src/frontend/gtk/window/imp.rs
Normal file
110
src/frontend/gtk/window/imp.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use std::cell::{Cell, RefCell};
|
||||
|
||||
#[cfg(windows)]
|
||||
use std::net::TcpStream;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
use adw::subclass::prelude::*;
|
||||
use adw::{
|
||||
prelude::{EditableExt, WidgetExt},
|
||||
ActionRow, ToastOverlay,
|
||||
};
|
||||
use glib::subclass::InitializingObject;
|
||||
use gtk::{gio, glib, Button, CompositeTemplate, Entry, ListBox};
|
||||
|
||||
use crate::config::DEFAULT_PORT;
|
||||
|
||||
#[derive(CompositeTemplate, Default)]
|
||||
#[template(resource = "/de/feschber/LanMouse/window.ui")]
|
||||
pub struct Window {
|
||||
#[template_child]
|
||||
pub port_edit_apply: TemplateChild<Button>,
|
||||
#[template_child]
|
||||
pub port_edit_cancel: TemplateChild<Button>,
|
||||
#[template_child]
|
||||
pub client_list: TemplateChild<ListBox>,
|
||||
#[template_child]
|
||||
pub client_placeholder: TemplateChild<ActionRow>,
|
||||
#[template_child]
|
||||
pub port_entry: TemplateChild<Entry>,
|
||||
#[template_child]
|
||||
pub toast_overlay: TemplateChild<ToastOverlay>,
|
||||
pub clients: RefCell<Option<gio::ListStore>>,
|
||||
#[cfg(unix)]
|
||||
pub stream: RefCell<Option<UnixStream>>,
|
||||
#[cfg(windows)]
|
||||
pub stream: RefCell<Option<TcpStream>>,
|
||||
pub port: Cell<u16>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for Window {
|
||||
// `NAME` needs to match `class` attribute of template
|
||||
const NAME: &'static str = "LanMouseWindow";
|
||||
type Type = super::Window;
|
||||
type ParentType = adw::ApplicationWindow;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.bind_template();
|
||||
klass.bind_template_callbacks();
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl Window {
|
||||
#[template_callback]
|
||||
fn handle_add_client_pressed(&self, _button: &Button) {
|
||||
self.obj().request_client_create();
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn handle_port_changed(&self, _entry: &Entry) {
|
||||
self.port_edit_apply.set_visible(true);
|
||||
self.port_edit_cancel.set_visible(true);
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn handle_port_edit_apply(&self) {
|
||||
self.obj().request_port_change();
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn handle_port_edit_cancel(&self) {
|
||||
log::debug!("cancel port edit");
|
||||
self.port_entry
|
||||
.set_text(self.port.get().to_string().as_str());
|
||||
self.port_edit_apply.set_visible(false);
|
||||
self.port_edit_cancel.set_visible(false);
|
||||
}
|
||||
|
||||
pub fn set_port(&self, port: u16) {
|
||||
self.port.set(port);
|
||||
if port == DEFAULT_PORT {
|
||||
self.port_entry.set_text("");
|
||||
} else {
|
||||
self.port_entry.set_text(format!("{port}").as_str());
|
||||
}
|
||||
self.port_edit_apply.set_visible(false);
|
||||
self.port_edit_cancel.set_visible(false);
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for Window {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
self.set_port(DEFAULT_PORT);
|
||||
let obj = self.obj();
|
||||
obj.setup_icon();
|
||||
obj.setup_clients();
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for Window {}
|
||||
impl WindowImpl for Window {}
|
||||
impl ApplicationWindowImpl for Window {}
|
||||
impl AdwApplicationWindowImpl for Window {}
|
||||
@@ -1,49 +0,0 @@
|
||||
use std::io::{self, Write};
|
||||
|
||||
use crate::client::Position;
|
||||
|
||||
|
||||
pub fn ask_confirmation(default: bool) -> Result<bool, io::Error> {
|
||||
eprint!("{}", if default {" [Y,n] "} else { " [y,N] "});
|
||||
io::stderr().flush()?;
|
||||
let answer = loop {
|
||||
let mut buffer = String::new();
|
||||
io::stdin().read_line(&mut buffer)?;
|
||||
let answer = buffer.to_lowercase();
|
||||
let answer = answer.trim();
|
||||
match answer {
|
||||
"" => break default,
|
||||
"y" => break true,
|
||||
"n" => break false,
|
||||
_ => {
|
||||
eprint!("Enter y for Yes or n for No: ");
|
||||
io::stderr().flush()?;
|
||||
continue
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(answer)
|
||||
}
|
||||
|
||||
pub fn ask_position() -> Result<Position, io::Error> {
|
||||
eprint!("Enter position - top (t) | bottom (b) | left(l) | right(r): ");
|
||||
io::stderr().flush()?;
|
||||
let pos = loop {
|
||||
let mut buffer = String::new();
|
||||
io::stdin().read_line(&mut buffer)?;
|
||||
let answer = buffer.to_lowercase();
|
||||
let answer = answer.trim();
|
||||
match answer {
|
||||
"t" | "top" => break Position::Top,
|
||||
"b" | "bottom" => break Position::Bottom,
|
||||
"l" | "left" => break Position::Right,
|
||||
"r" | "right" => break Position::Left,
|
||||
_ => {
|
||||
eprint!("Invalid position: {answer} - enter top (t) | bottom (b) | left(l) | right(r): ");
|
||||
io::stderr().flush()?;
|
||||
continue
|
||||
}
|
||||
};
|
||||
};
|
||||
Ok(pos)
|
||||
}
|
||||
@@ -2,10 +2,11 @@ pub mod client;
|
||||
pub mod config;
|
||||
pub mod dns;
|
||||
pub mod event;
|
||||
pub mod server;
|
||||
|
||||
pub mod consumer;
|
||||
pub mod producer;
|
||||
|
||||
pub mod backend;
|
||||
pub mod frontend;
|
||||
pub mod ioutils;
|
||||
pub mod scancode;
|
||||
|
||||
95
src/main.rs
95
src/main.rs
@@ -1,14 +1,12 @@
|
||||
use std::{process, error::Error};
|
||||
use anyhow::Result;
|
||||
use std::process::{self, Child, Command};
|
||||
|
||||
use env_logger::Env;
|
||||
use lan_mouse::{
|
||||
consumer, producer,
|
||||
config::{Config, Frontend::{Gtk, Cli}}, event::server::Server,
|
||||
frontend::{FrontendAdapter, cli::CliFrontend},
|
||||
};
|
||||
use lan_mouse::{config::Config, frontend, server::Server};
|
||||
|
||||
use tokio::task::LocalSet;
|
||||
|
||||
pub fn main() {
|
||||
|
||||
// init logging
|
||||
let env = Env::default().filter_or("LAN_MOUSE_LOG_LEVEL", "info");
|
||||
env_logger::init_from_env(env);
|
||||
@@ -19,47 +17,58 @@ pub fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), Box<dyn Error>> {
|
||||
// parse config file
|
||||
pub fn start_service() -> Result<Child> {
|
||||
let child = Command::new(std::env::current_exe()?)
|
||||
.args(std::env::args().skip(1))
|
||||
.arg("--daemon")
|
||||
.spawn()?;
|
||||
Ok(child)
|
||||
}
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
// parse config file + cli args
|
||||
let config = Config::new()?;
|
||||
log::debug!("{config:?}");
|
||||
|
||||
// start producing and consuming events
|
||||
let producer = producer::create()?;
|
||||
let consumer = consumer::create()?;
|
||||
|
||||
// create frontend communication adapter
|
||||
let frontend_adapter = FrontendAdapter::new()?;
|
||||
|
||||
// start sending and receiving events
|
||||
let mut event_server = Server::new(config.port, producer, consumer, frontend_adapter)?;
|
||||
|
||||
// add clients form config
|
||||
config.get_clients().into_iter().for_each(|(c, h, p)| {
|
||||
let host_name = match h {
|
||||
Some(h) => format!(" '{}'", h),
|
||||
None => "".to_owned(),
|
||||
};
|
||||
if c.len() == 0 {
|
||||
log::warn!("ignoring client{} with 0 assigned ips!", host_name);
|
||||
if config.daemon {
|
||||
// if daemon is specified we run the service
|
||||
run_service(&config)?;
|
||||
} else {
|
||||
// otherwise start the service as a child process and
|
||||
// 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
|
||||
let pid = service.id() as libc::pid_t;
|
||||
unsafe {
|
||||
libc::kill(pid, libc::SIGINT);
|
||||
}
|
||||
service.wait()?;
|
||||
}
|
||||
log::info!("adding client [{}]{} @ {:?}", p, host_name, c);
|
||||
event_server.add_client(c, p);
|
||||
});
|
||||
service.kill()?;
|
||||
}
|
||||
|
||||
// any threads need to be started after event_server sets up signal handling
|
||||
match config.frontend {
|
||||
Gtk => {
|
||||
#[cfg(all(unix, feature = "gtk"))]
|
||||
frontend::gtk::create();
|
||||
#[cfg(not(feature = "gtk"))]
|
||||
panic!("gtk frontend requested but feature not enabled!");
|
||||
},
|
||||
Cli => Box::new(CliFrontend::new()?),
|
||||
};
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
log::info!("Press Ctrl+Alt+Shift+Super to release the mouse");
|
||||
// run event loop
|
||||
event_server.run()?;
|
||||
fn run_service(config: &Config) -> Result<()> {
|
||||
// create single threaded tokio runtime
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
.enable_time()
|
||||
.build()?;
|
||||
|
||||
// run async event loop
|
||||
runtime.block_on(LocalSet::new().run_until(async {
|
||||
// run main loop
|
||||
log::info!("Press Ctrl+Alt+Shift+Super to release the mouse");
|
||||
Server::run(config).await?;
|
||||
|
||||
log::debug!("service exiting");
|
||||
anyhow::Ok(())
|
||||
}))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
102
src/producer.rs
102
src/producer.rs
@@ -1,63 +1,61 @@
|
||||
use mio::event::Source;
|
||||
use std::{error::Error, vec::Drain};
|
||||
use crate::{client::{ClientHandle, ClientEvent}, event::Event};
|
||||
use std::io;
|
||||
|
||||
use futures_core::Stream;
|
||||
|
||||
use crate::backend::producer;
|
||||
use crate::{
|
||||
client::{ClientEvent, ClientHandle},
|
||||
event::Event,
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::env;
|
||||
|
||||
#[cfg(unix)]
|
||||
enum Backend {
|
||||
Wayland,
|
||||
X11,
|
||||
}
|
||||
|
||||
pub fn create() -> Result<Box<dyn EventProducer>, Box<dyn Error>> {
|
||||
#[cfg(windows)]
|
||||
return Ok(Box::new(producer::windows::WindowsProducer::new()));
|
||||
|
||||
#[cfg(unix)]
|
||||
let backend = match env::var("XDG_SESSION_TYPE") {
|
||||
Ok(session_type) => match session_type.as_str() {
|
||||
"x11" => {
|
||||
log::info!("XDG_SESSION_TYPE = x11 -> using X11 event producer");
|
||||
Backend::X11
|
||||
},
|
||||
"wayland" => {
|
||||
log::info!("XDG_SESSION_TYPE = wayland -> using wayland event producer");
|
||||
Backend::Wayland
|
||||
}
|
||||
_ => panic!("unknown XDG_SESSION_TYPE"),
|
||||
},
|
||||
Err(_) => panic!("could not detect session type: XDG_SESSION_TYPE environment variable not set!"),
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
match backend {
|
||||
Backend::X11 => {
|
||||
#[cfg(not(feature = "x11"))]
|
||||
panic!("feature x11 not enabled");
|
||||
#[cfg(feature = "x11")]
|
||||
Ok(Box::new(producer::x11::X11Producer::new()))
|
||||
}
|
||||
Backend::Wayland => {
|
||||
#[cfg(not(feature = "wayland"))]
|
||||
panic!("feature wayland not enabled");
|
||||
#[cfg(feature = "wayland")]
|
||||
Ok(Box::new(producer::wayland::WaylandEventProducer::new()?))
|
||||
}
|
||||
pub async fn create() -> Box<dyn EventProducer> {
|
||||
#[cfg(target_os = "macos")]
|
||||
match producer::macos::MacOSProducer::new() {
|
||||
Ok(p) => return Box::new(p),
|
||||
Err(e) => log::info!("macos event producer not available: {e}"),
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
match producer::windows::WindowsProducer::new() {
|
||||
Ok(p) => return Box::new(p),
|
||||
Err(e) => log::info!("windows event producer not available: {e}"),
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
match producer::libei::LibeiProducer::new() {
|
||||
Ok(p) => {
|
||||
log::info!("using libei event producer");
|
||||
return Box::new(p);
|
||||
}
|
||||
Err(e) => log::info!("libei event producer not available: {e}"),
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
match producer::wayland::WaylandEventProducer::new() {
|
||||
Ok(p) => {
|
||||
log::info!("using layer-shell event producer");
|
||||
return Box::new(p);
|
||||
}
|
||||
Err(e) => log::info!("layer_shell event producer not available: {e}"),
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
match producer::x11::X11Producer::new() {
|
||||
Ok(p) => {
|
||||
log::info!("using x11 event producer");
|
||||
return Box::new(p);
|
||||
}
|
||||
Err(e) => log::info!("x11 event producer not available: {e}"),
|
||||
}
|
||||
|
||||
log::error!("falling back to dummy event producer");
|
||||
Box::new(producer::dummy::DummyProducer::new())
|
||||
}
|
||||
|
||||
pub trait EventProducer: Source {
|
||||
pub trait EventProducer: Stream<Item = io::Result<(ClientHandle, Event)>> + Unpin {
|
||||
/// notify event producer of configuration changes
|
||||
fn notify(&mut self, event: ClientEvent);
|
||||
|
||||
/// read an event
|
||||
/// this function must be invoked to retrieve an Event after
|
||||
/// the eventfd indicates a pending Event
|
||||
fn read_events(&mut self) -> Drain<(ClientHandle, Event)>;
|
||||
|
||||
/// release mouse
|
||||
fn release(&mut self);
|
||||
}
|
||||
|
||||
698
src/scancode.rs
Normal file
698
src/scancode.rs
Normal file
@@ -0,0 +1,698 @@
|
||||
/*
|
||||
* https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input
|
||||
*/
|
||||
#[repr(u32)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Windows {
|
||||
Shutdown = 0xE05E,
|
||||
SystemSleep = 0xE05F,
|
||||
SystemWakeUp = 0xE063,
|
||||
ErrorRollOver = 0x00FF,
|
||||
KeyA = 0x001E,
|
||||
KeyB = 0x0030,
|
||||
KeyC = 0x002E,
|
||||
KeyD = 0x0020,
|
||||
KeyE = 0x0012,
|
||||
KeyF = 0x0021,
|
||||
KeyG = 0x0022,
|
||||
KeyH = 0x0023,
|
||||
KeyI = 0x0017,
|
||||
KeyJ = 0x0024,
|
||||
KeyK = 0x0025,
|
||||
KeyL = 0x0026,
|
||||
KeyM = 0x0032,
|
||||
KeyN = 0x0031,
|
||||
KeyO = 0x0018,
|
||||
KeyP = 0x0019,
|
||||
KeyQ = 0x0010,
|
||||
KeyR = 0x0013,
|
||||
KeyS = 0x001F,
|
||||
KeyT = 0x0014,
|
||||
KeyU = 0x0016,
|
||||
KeyV = 0x002F,
|
||||
KeyW = 0x0011,
|
||||
KeyX = 0x002D,
|
||||
KeyY = 0x0015,
|
||||
KeyZ = 0x002C,
|
||||
Key1 = 0x0002,
|
||||
Key2 = 0x0003,
|
||||
Key3 = 0x0004,
|
||||
Key4 = 0x0005,
|
||||
Key5 = 0x0006,
|
||||
Key6 = 0x0007,
|
||||
Key7 = 0x0008,
|
||||
Key8 = 0x0009,
|
||||
Key9 = 0x000A,
|
||||
Key0 = 0x000B,
|
||||
KeyEnter = 0x001C,
|
||||
KeyEsc = 0x0001,
|
||||
KeyDelete = 0x000E,
|
||||
KeyTab = 0x000F,
|
||||
KeySpace = 0x0039,
|
||||
KeyMinus = 0x000C,
|
||||
KeyEqual = 0x000D,
|
||||
KeyLeftBrace = 0x001A,
|
||||
KeyRightBrace = 0x001B,
|
||||
KeyBackslash = 0x002B,
|
||||
KeySemiColon = 0x0027,
|
||||
KeyApostrophe = 0x0028,
|
||||
KeyGrave = 0x0029,
|
||||
KeyComma = 0x0033,
|
||||
KeyDot = 0x0034,
|
||||
KeySlash = 0x0035,
|
||||
KeyCapsLock = 0x003A,
|
||||
KeyF1 = 0x003B,
|
||||
KeyF2 = 0x003C,
|
||||
KeyF3 = 0x003D,
|
||||
KeyF4 = 0x003E,
|
||||
KeyF5 = 0x003F,
|
||||
KeyF6 = 0x0040,
|
||||
KeyF7 = 0x0041,
|
||||
KeyF8 = 0x0042,
|
||||
KeyF9 = 0x0043,
|
||||
KeyF10 = 0x0044,
|
||||
KeyF11 = 0x0057,
|
||||
KeyF12 = 0x0058,
|
||||
KeyPrintScreen = 0xE037,
|
||||
KeyScrollLock = 0x0046,
|
||||
KeyPause = 0xE11D45,
|
||||
KeyInsert = 0xE052,
|
||||
KeyHome = 0xE047,
|
||||
KeyPageUp = 0xE049,
|
||||
KeyDeleteForward = 0xE053,
|
||||
KeyEnd = 0xE04F,
|
||||
KeyPageDown = 0xE051,
|
||||
KeyRight = 0xE04D,
|
||||
KeyLeft = 0xE04B,
|
||||
KeyDown = 0xE050,
|
||||
KeyUp = 0xE048,
|
||||
KeypadNumLock = 0x0045,
|
||||
KeypadSlash = 0xE035,
|
||||
KeypadStar = 0x0037,
|
||||
KeypadDash = 0x004A,
|
||||
KeypadPlus = 0x004E,
|
||||
KeypadEnter = 0xE01C,
|
||||
Keypad1End = 0x004F,
|
||||
Keypad2DownArrow = 0x0050,
|
||||
Keypad3PageDn = 0x0051,
|
||||
Keypad4LeftArrow = 0x004B,
|
||||
Keypad5 = 0x004C,
|
||||
Keypad6RightArrow = 0x004D,
|
||||
Keypad7Home = 0x0047,
|
||||
Keypad8UpArrow = 0x0048,
|
||||
Keypad9PageUp = 0x0049,
|
||||
Keypad0Insert = 0x0052,
|
||||
KeypadDot = 0x0053,
|
||||
KeyNonUSSlashBar = 0x0056,
|
||||
KeyApplication = 0xE05D,
|
||||
KeypadEquals = 0x0059,
|
||||
KeyF13 = 0x0064,
|
||||
KeyF14 = 0x0065,
|
||||
KeyF15 = 0x0066,
|
||||
KeyF16 = 0x0067,
|
||||
KeyF17 = 0x0068,
|
||||
KeyF18 = 0x0069,
|
||||
KeyF19 = 0x006A,
|
||||
KeyF20 = 0x006B,
|
||||
KeyF21 = 0x006C,
|
||||
KeyF22 = 0x006D,
|
||||
KeyF23 = 0x006E,
|
||||
KeyF24 = 0x0076,
|
||||
KeypadComma = 0x007E,
|
||||
KeyInternational1 = 0x0073,
|
||||
KeyInternational2 = 0x0070,
|
||||
KeyInternational3 = 0x007D,
|
||||
#[allow(dead_code)]
|
||||
KeyInternational4 = 0x0079, // FIXME unused
|
||||
#[allow(dead_code)]
|
||||
KeyInternational5 = 0x007B, // FIXME unused
|
||||
// KeyInternational6 = 0x005C,
|
||||
KeyLANG1 = 0x0072,
|
||||
KeyLANG2 = 0x0071,
|
||||
KeyLANG3 = 0x0078,
|
||||
KeyLANG4 = 0x0077,
|
||||
// KeyLANG5 = 0x0076,
|
||||
KeyLeftCtrl = 0x001D,
|
||||
KeyLeftShift = 0x002A,
|
||||
KeyLeftAlt = 0x0038,
|
||||
KeyLeftGUI = 0xE05B,
|
||||
KeyRightCtrl = 0xE01D,
|
||||
KeyRightShift = 0x0036,
|
||||
KeyRightAlt = 0xE038,
|
||||
KeyRightGUI = 0xE05C,
|
||||
KeyScanNextTrack = 0xE019,
|
||||
KeyScanPreviousTrack = 0xE010,
|
||||
KeyStop = 0xE024,
|
||||
KeyPlayPause = 0xE022,
|
||||
KeyMute = 0xE020,
|
||||
KeyVolumeUp = 0xE030,
|
||||
KeyVolumeDown = 0xE02E,
|
||||
#[allow(dead_code)]
|
||||
ALConsumerControlConfiguration = 0xE06D, // TODO Unused
|
||||
ALEmailReader = 0xE06C,
|
||||
ALCalculator = 0xE021,
|
||||
ALLocalMachineBrowser = 0xE06B,
|
||||
ACSearch = 0xE065,
|
||||
ACHome = 0xE032,
|
||||
ACBack = 0xE06A,
|
||||
ACForward = 0xE069,
|
||||
ACStop = 0xE068,
|
||||
ACRefresh = 0xE067,
|
||||
ACBookmarks = 0xE066,
|
||||
}
|
||||
|
||||
/*
|
||||
* https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h
|
||||
*/
|
||||
#[repr(u32)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[allow(dead_code)]
|
||||
pub enum Linux {
|
||||
KeyReserved = 0,
|
||||
KeyEsc = 1,
|
||||
Key1 = 2,
|
||||
Key2 = 3,
|
||||
Key3 = 4,
|
||||
Key4 = 5,
|
||||
Key5 = 6,
|
||||
Key6 = 7,
|
||||
Key7 = 8,
|
||||
Key8 = 9,
|
||||
Key9 = 10,
|
||||
Key0 = 11,
|
||||
KeyMinus = 12,
|
||||
KeyEqual = 13,
|
||||
KeyBackspace = 14,
|
||||
KeyTab = 15,
|
||||
KeyQ = 16,
|
||||
KeyW = 17,
|
||||
KeyE = 18,
|
||||
KeyR = 19,
|
||||
KeyT = 20,
|
||||
KeyY = 21,
|
||||
KeyU = 22,
|
||||
KeyI = 23,
|
||||
KeyO = 24,
|
||||
KeyP = 25,
|
||||
KeyLeftbrace = 26,
|
||||
KeyRightbrace = 27,
|
||||
KeyEnter = 28,
|
||||
KeyLeftCtrl = 29,
|
||||
KeyA = 30,
|
||||
KeyS = 31,
|
||||
KeyD = 32,
|
||||
KeyF = 33,
|
||||
KeyG = 34,
|
||||
KeyH = 35,
|
||||
KeyJ = 36,
|
||||
KeyK = 37,
|
||||
KeyL = 38,
|
||||
KeySemicolon = 39,
|
||||
KeyApostrophe = 40,
|
||||
KeyGrave = 41,
|
||||
KeyLeftshift = 42,
|
||||
KeyBackslash = 43,
|
||||
KeyZ = 44,
|
||||
KeyX = 45,
|
||||
KeyC = 46,
|
||||
KeyV = 47,
|
||||
KeyB = 48,
|
||||
KeyN = 49,
|
||||
KeyM = 50,
|
||||
KeyComma = 51,
|
||||
KeyDot = 52,
|
||||
KeySlash = 53,
|
||||
KeyRightShift = 54,
|
||||
KeyKpAsterisk = 55,
|
||||
KeyLeftalt = 56,
|
||||
KeySpace = 57,
|
||||
KeyCapsLock = 58,
|
||||
KeyF1 = 59,
|
||||
KeyF2 = 60,
|
||||
KeyF3 = 61,
|
||||
KeyF4 = 62,
|
||||
KeyF5 = 63,
|
||||
KeyF6 = 64,
|
||||
KeyF7 = 65,
|
||||
KeyF8 = 66,
|
||||
KeyF9 = 67,
|
||||
KeyF10 = 68,
|
||||
KeyNumlock = 69,
|
||||
KeyScrollLock = 70,
|
||||
KeyKp7 = 71,
|
||||
KeyKp8 = 72,
|
||||
KeyKp9 = 73,
|
||||
KeyKpMinus = 74,
|
||||
KeyKp4 = 75,
|
||||
KeyKp5 = 76,
|
||||
KeyKp6 = 77,
|
||||
KeyKpplus = 78,
|
||||
KeyKp1 = 79,
|
||||
KeyKp2 = 80,
|
||||
KeyKp3 = 81,
|
||||
KeyKp0 = 82,
|
||||
KeyKpDot = 83,
|
||||
Invalid = 84,
|
||||
KeyZenkakuhankaku = 85,
|
||||
Key102nd = 86,
|
||||
KeyF11 = 87,
|
||||
KeyF12 = 88,
|
||||
KeyRo = 89,
|
||||
KeyKatakana = 90,
|
||||
KeyHiragana = 91,
|
||||
KeyHenkan = 92,
|
||||
KeyKatakanahiragana = 93,
|
||||
KeyMuhenkan = 94,
|
||||
KeyKpJpComma = 95,
|
||||
KeyKpEnter = 96,
|
||||
KeyRightCtrl = 97,
|
||||
KeyKpslash = 98,
|
||||
KeySysrq = 99,
|
||||
KeyRightalt = 100,
|
||||
KeyLinefeed = 101,
|
||||
KeyHome = 102,
|
||||
KeyUp = 103,
|
||||
KeyPageup = 104,
|
||||
KeyLeft = 105,
|
||||
KeyRight = 106,
|
||||
KeyEnd = 107,
|
||||
KeyDown = 108,
|
||||
KeyPagedown = 109,
|
||||
KeyInsert = 110,
|
||||
KeyDelete = 111,
|
||||
KeyMacro = 112,
|
||||
KeyMute = 113,
|
||||
KeyVolumeDown = 114,
|
||||
KeyVolumeUp = 115,
|
||||
KeyPower = 116, /* SC System Power Down */
|
||||
KeyKpequal = 117,
|
||||
KeyKpplusminus = 118,
|
||||
KeyPause = 119,
|
||||
KeyScale = 120, /* AL Compiz Scale (Expose) */
|
||||
KeyKpcomma = 121,
|
||||
KeyHangeul = 122,
|
||||
// KEY_HANGUEL = KeyHangeul,
|
||||
KeyHanja = 123,
|
||||
KeyYen = 124,
|
||||
KeyLeftmeta = 125,
|
||||
KeyRightmeta = 126,
|
||||
KeyCompose = 127,
|
||||
KeyStop = 128, /* AC Stop */
|
||||
KeyAgain = 129,
|
||||
KeyProps = 130, /* AC Properties */
|
||||
KeyUndo = 131, /* AC Undo */
|
||||
KeyFront = 132,
|
||||
KeyCopy = 133, /* AC Copy */
|
||||
KeyOpen = 134, /* AC Open */
|
||||
KeyPaste = 135, /* AC Paste */
|
||||
KeyFind = 136, /* AC Search */
|
||||
KeyCut = 137, /* AC Cut */
|
||||
KeyHelp = 138, /* AL Integrated Help Center */
|
||||
KeyMenu = 139, /* Menu (show menu) */
|
||||
KeyCalc = 140, /* AL Calculator */
|
||||
KeySetup = 141,
|
||||
KeySleep = 142, /* SC System Sleep */
|
||||
KeyWakeup = 143, /* System Wake Up */
|
||||
KeyFile = 144, /* AL Local Machine Browser */
|
||||
KeySendfile = 145,
|
||||
KeyDeletefile = 146,
|
||||
KeyXfer = 147,
|
||||
KeyProg1 = 148,
|
||||
KeyProg2 = 149,
|
||||
KeyWww = 150, /* AL Internet Browser */
|
||||
KeyMsdos = 151,
|
||||
KeyCoffee = 152, /* AL Terminal Lock/Screensaver */
|
||||
// KEY_SCREENLOCK = KeyCoffee,
|
||||
KeyRotateDisplay = 153, /* Display orientation for e.g. tablets */
|
||||
// KEY_DIRECTION = KeyRotateDisplay,
|
||||
KeyCyclewindows = 154,
|
||||
KeyMail = 155,
|
||||
KeyBookmarks = 156, /* AC Bookmarks */
|
||||
KeyComputer = 157,
|
||||
KeyBack = 158, /* AC Back */
|
||||
KeyForward = 159, /* AC Forward */
|
||||
KeyClosecd = 160,
|
||||
KeyEjectcd = 161,
|
||||
KeyEjectclosecd = 162,
|
||||
KeyNextsong = 163,
|
||||
KeyPlaypause = 164,
|
||||
KeyPrevioussong = 165,
|
||||
KeyStopcd = 166,
|
||||
KeyRecord = 167,
|
||||
KeyRewind = 168,
|
||||
KeyPhone = 169, /* Media Select Telephone */
|
||||
KeyIso = 170,
|
||||
KeyConfig = 171, /* AL Consumer Control Configuration */
|
||||
KeyHomepage = 172, /* AC Home */
|
||||
KeyRefresh = 173, /* AC Refresh */
|
||||
KeyExit = 174, /* AC Exit */
|
||||
KeyMove = 175,
|
||||
KeyEdit = 176,
|
||||
KeyScrollup = 177,
|
||||
KeyScrolldown = 178,
|
||||
KeyKpleftparen = 179,
|
||||
KeyKprightparen = 180,
|
||||
KeyNew = 181, /* AC New */
|
||||
KeyRedo = 182, /* AC Redo/Repeat */
|
||||
KeyF13 = 183,
|
||||
KeyF14 = 184,
|
||||
KeyF15 = 185,
|
||||
KeyF16 = 186,
|
||||
KeyF17 = 187,
|
||||
KeyF18 = 188,
|
||||
KeyF19 = 189,
|
||||
KeyF20 = 190,
|
||||
KeyF21 = 191,
|
||||
KeyF22 = 192,
|
||||
KeyF23 = 193,
|
||||
KeyF24 = 194,
|
||||
Invalid1 = 195,
|
||||
Invalid2 = 196,
|
||||
Invalid3 = 197,
|
||||
Invalid4 = 198,
|
||||
Invalid5 = 199,
|
||||
KeyPlaycd = 200,
|
||||
KeyPausecd = 201,
|
||||
KeyProg3 = 202,
|
||||
KeyProg4 = 203,
|
||||
KeyAllApplications = 204, /* AC Desktop Show All Applications */
|
||||
// KEY_DASHBOARD = KeyAllApplications,
|
||||
KeySuspend = 205,
|
||||
KeyClose = 206, /* AC Close */
|
||||
KeyPlay = 207,
|
||||
KeyFastforward = 208,
|
||||
KeyBassboost = 209,
|
||||
KeyPrint = 210, /* AC Print */
|
||||
KeyHp = 211,
|
||||
KeyCamera = 212,
|
||||
KeySound = 213,
|
||||
KeyQuestion = 214,
|
||||
KeyEmail = 215,
|
||||
KeyChat = 216,
|
||||
KeySearch = 217,
|
||||
KeyConnect = 218,
|
||||
KeyFinance = 219, /* AL Checkbook/Finance */
|
||||
KeySport = 220,
|
||||
KeyShop = 221,
|
||||
KeyAlterase = 222,
|
||||
KeyCancel = 223, /* AC Cancel */
|
||||
KeyBrightnessdown = 224,
|
||||
KeyBrightnessup = 225,
|
||||
KeyMedia = 226,
|
||||
KeySwitchvideomode = 227, /* Cycle between available video, outputs (Monitor/LCD/TV-out/etc) */
|
||||
KeyKbdillumtoggle = 228,
|
||||
KeyKbdillumdown = 229,
|
||||
KeyKbdillumup = 230,
|
||||
KeySend = 231, /* AC Send */
|
||||
KeyReply = 232, /* AC Reply */
|
||||
KeyForwardmail = 233, /* AC Forward Msg */
|
||||
KeySave = 234, /* AC Save */
|
||||
KeyDocuments = 235,
|
||||
KeyBattery = 236,
|
||||
KeyBluetooth = 237,
|
||||
KeyWlan = 238,
|
||||
KeyUwb = 239,
|
||||
KeyUnknown = 240,
|
||||
KeyVideoNext = 241, /* drive next video source */
|
||||
KeyVideoPrev = 242, /* drive previous video source */
|
||||
KeyBrightnessCycle = 243, /* brightness up, after max is min */
|
||||
KeyBrightnessAuto = 244, /* Set Auto Brightness: manual, brightness control is off, rely on ambient */
|
||||
// KEY_BRIGHTNESS_ZERO=KeyBrightnessAuto,
|
||||
KeyDisplayOff = 245, /* display device to off state */
|
||||
KeyWwan = 246, /* Wireless WAN (LTE, UMTS, GSM, etc.) */
|
||||
// KEY_WIMAX = KeyWwan,
|
||||
KeyRfkill = 247, /* Key that controls all radios */
|
||||
KeyMicmute = 248, /* Mute / unmute the microphone */
|
||||
KeyCount = 249,
|
||||
}
|
||||
|
||||
impl TryFrom<u32> for Linux {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
||||
if value >= Self::KeyCount as u32 {
|
||||
return Err(());
|
||||
}
|
||||
let code: Linux = unsafe { std::mem::transmute(value) };
|
||||
Ok(code)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Linux> for Windows {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Linux) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
Linux::KeyReserved => Err(()),
|
||||
Linux::KeyEsc => Ok(Self::KeyEsc),
|
||||
Linux::Key1 => Ok(Self::Key1),
|
||||
Linux::Key2 => Ok(Self::Key2),
|
||||
Linux::Key3 => Ok(Self::Key3),
|
||||
Linux::Key4 => Ok(Self::Key4),
|
||||
Linux::Key5 => Ok(Self::Key5),
|
||||
Linux::Key6 => Ok(Self::Key6),
|
||||
Linux::Key7 => Ok(Self::Key7),
|
||||
Linux::Key8 => Ok(Self::Key8),
|
||||
Linux::Key9 => Ok(Self::Key9),
|
||||
Linux::Key0 => Ok(Self::Key0),
|
||||
Linux::KeyMinus => Ok(Self::KeyMinus),
|
||||
Linux::KeyEqual => Ok(Self::KeyEqual),
|
||||
Linux::KeyBackspace => Ok(Self::KeyDelete),
|
||||
Linux::KeyTab => Ok(Self::KeyTab),
|
||||
Linux::KeyQ => Ok(Self::KeyQ),
|
||||
Linux::KeyW => Ok(Self::KeyW),
|
||||
Linux::KeyE => Ok(Self::KeyE),
|
||||
Linux::KeyR => Ok(Self::KeyR),
|
||||
Linux::KeyT => Ok(Self::KeyT),
|
||||
Linux::KeyY => Ok(Self::KeyY),
|
||||
Linux::KeyU => Ok(Self::KeyU),
|
||||
Linux::KeyI => Ok(Self::KeyI),
|
||||
Linux::KeyO => Ok(Self::KeyO),
|
||||
Linux::KeyP => Ok(Self::KeyP),
|
||||
Linux::KeyLeftbrace => Ok(Self::KeyLeftBrace),
|
||||
Linux::KeyRightbrace => Ok(Self::KeyRightBrace),
|
||||
Linux::KeyEnter => Ok(Self::KeyEnter),
|
||||
Linux::KeyLeftCtrl => Ok(Self::KeyLeftCtrl),
|
||||
Linux::KeyA => Ok(Self::KeyA),
|
||||
Linux::KeyS => Ok(Self::KeyS),
|
||||
Linux::KeyD => Ok(Self::KeyD),
|
||||
Linux::KeyF => Ok(Self::KeyF),
|
||||
Linux::KeyG => Ok(Self::KeyG),
|
||||
Linux::KeyH => Ok(Self::KeyH),
|
||||
Linux::KeyJ => Ok(Self::KeyJ),
|
||||
Linux::KeyK => Ok(Self::KeyK),
|
||||
Linux::KeyL => Ok(Self::KeyL),
|
||||
Linux::KeySemicolon => Ok(Self::KeySemiColon),
|
||||
Linux::KeyApostrophe => Ok(Self::KeyApostrophe),
|
||||
Linux::KeyGrave => Ok(Self::KeyGrave),
|
||||
Linux::KeyLeftshift => Ok(Self::KeyLeftShift),
|
||||
Linux::KeyBackslash => Ok(Self::KeyBackslash),
|
||||
Linux::KeyZ => Ok(Self::KeyZ),
|
||||
Linux::KeyX => Ok(Self::KeyX),
|
||||
Linux::KeyC => Ok(Self::KeyC),
|
||||
Linux::KeyV => Ok(Self::KeyV),
|
||||
Linux::KeyB => Ok(Self::KeyB),
|
||||
Linux::KeyN => Ok(Self::KeyN),
|
||||
Linux::KeyM => Ok(Self::KeyM),
|
||||
Linux::KeyComma => Ok(Self::KeyComma),
|
||||
Linux::KeyDot => Ok(Self::KeyDot),
|
||||
Linux::KeySlash => Ok(Self::KeySlash),
|
||||
Linux::KeyRightShift => Ok(Self::KeyRightShift),
|
||||
Linux::KeyKpAsterisk => Ok(Self::KeypadStar),
|
||||
Linux::KeyLeftalt => Ok(Self::KeyLeftAlt),
|
||||
Linux::KeySpace => Ok(Self::KeySpace),
|
||||
Linux::KeyCapsLock => Ok(Self::KeyCapsLock),
|
||||
Linux::KeyF1 => Ok(Self::KeyF1),
|
||||
Linux::KeyF2 => Ok(Self::KeyF2),
|
||||
Linux::KeyF3 => Ok(Self::KeyF3),
|
||||
Linux::KeyF4 => Ok(Self::KeyF4),
|
||||
Linux::KeyF5 => Ok(Self::KeyF5),
|
||||
Linux::KeyF6 => Ok(Self::KeyF6),
|
||||
Linux::KeyF7 => Ok(Self::KeyF7),
|
||||
Linux::KeyF8 => Ok(Self::KeyF8),
|
||||
Linux::KeyF9 => Ok(Self::KeyF9),
|
||||
Linux::KeyF10 => Ok(Self::KeyF10),
|
||||
Linux::KeyNumlock => Ok(Self::KeypadNumLock),
|
||||
Linux::KeyScrollLock => Ok(Self::KeyScrollLock),
|
||||
Linux::KeyKp7 => Ok(Self::Keypad7Home),
|
||||
Linux::KeyKp8 => Ok(Self::Keypad8UpArrow),
|
||||
Linux::KeyKp9 => Ok(Self::Keypad9PageUp),
|
||||
Linux::KeyKpMinus => Ok(Self::KeypadDash),
|
||||
Linux::KeyKp4 => Ok(Self::Keypad4LeftArrow),
|
||||
Linux::KeyKp5 => Ok(Self::Keypad5),
|
||||
Linux::KeyKp6 => Ok(Self::Keypad6RightArrow),
|
||||
Linux::KeyKpplus => Ok(Self::KeypadPlus),
|
||||
Linux::KeyKp1 => Ok(Self::Keypad1End),
|
||||
Linux::KeyKp2 => Ok(Self::Keypad2DownArrow),
|
||||
Linux::KeyKp3 => Ok(Self::Keypad3PageDn),
|
||||
Linux::KeyKp0 => Ok(Self::Keypad0Insert),
|
||||
Linux::KeyKpDot => Ok(Self::KeypadDot),
|
||||
Linux::KeyZenkakuhankaku => Ok(Self::KeyLANG1), // TODO unsure
|
||||
Linux::Key102nd => Ok(Self::KeyNonUSSlashBar), // TODO unsure
|
||||
Linux::KeyF11 => Ok(Self::KeyF11),
|
||||
Linux::KeyF12 => Ok(Self::KeyF12),
|
||||
Linux::KeyRo => Ok(Self::ErrorRollOver), // TODO unsure
|
||||
Linux::KeyKatakana => Ok(Self::KeyLANG1), // TODO unsure
|
||||
Linux::KeyHiragana => Ok(Self::KeyLANG2), // TODO unsure
|
||||
Linux::KeyHenkan => Ok(Self::KeyLANG3), // TODO unsure
|
||||
Linux::KeyKatakanahiragana => Ok(Self::KeyLANG4), // TODO unsure
|
||||
Linux::KeyMuhenkan => Ok(Self::KeyLANG4), // TODO unsure
|
||||
Linux::KeyKpJpComma => Ok(Self::KeypadComma),
|
||||
Linux::KeyKpEnter => Ok(Self::KeypadEnter),
|
||||
Linux::KeyRightCtrl => Ok(Self::KeyRightCtrl),
|
||||
Linux::KeyKpslash => Ok(Self::KeypadSlash),
|
||||
Linux::KeySysrq => Ok(Self::KeyPrintScreen), // TODO Windows does not have Sysrq, right?
|
||||
Linux::KeyRightalt => Ok(Self::KeyRightAlt),
|
||||
Linux::KeyLinefeed => Ok(Self::KeyEnter), // TODO unsure
|
||||
Linux::KeyHome => Ok(Self::KeyHome),
|
||||
Linux::KeyUp => Ok(Self::KeyUp),
|
||||
Linux::KeyPageup => Ok(Self::KeyPageUp),
|
||||
Linux::KeyLeft => Ok(Self::KeyLeft),
|
||||
Linux::KeyRight => Ok(Self::KeyRight),
|
||||
Linux::KeyEnd => Ok(Self::KeyEnd),
|
||||
Linux::KeyDown => Ok(Self::KeyDown),
|
||||
Linux::KeyPagedown => Ok(Self::KeyPageDown),
|
||||
Linux::KeyInsert => Ok(Self::KeyInsert),
|
||||
Linux::KeyDelete => Ok(Self::KeyDeleteForward),
|
||||
Linux::KeyMacro => Err(()), // TODO
|
||||
Linux::KeyMute => Ok(Self::KeyMute),
|
||||
Linux::KeyVolumeDown => Ok(Self::KeyVolumeDown),
|
||||
Linux::KeyVolumeUp => Ok(Self::KeyVolumeUp),
|
||||
Linux::KeyPower => Ok(Self::Shutdown),
|
||||
Linux::KeyKpequal => Ok(Self::KeypadEquals),
|
||||
Linux::KeyKpplusminus => Ok(Self::KeypadPlus),
|
||||
Linux::KeyPause => Ok(Self::KeyPause),
|
||||
Linux::KeyScale => Err(()), // TODO
|
||||
Linux::KeyKpcomma => Ok(Self::KeypadComma),
|
||||
Linux::KeyHangeul => Ok(Self::KeyInternational1), // TODO unsure
|
||||
Linux::KeyHanja => Ok(Self::KeyInternational2), // TODO unsure
|
||||
Linux::KeyYen => Ok(Self::KeyInternational3), // TODO unsure
|
||||
Linux::KeyLeftmeta => Ok(Self::KeyLeftGUI),
|
||||
Linux::KeyRightmeta => Ok(Self::KeyRightGUI),
|
||||
Linux::KeyCompose => Ok(Self::KeyApplication),
|
||||
Linux::KeyStop => Ok(Self::ACStop),
|
||||
Linux::KeyAgain => Err(()),
|
||||
Linux::KeyProps => Err(()),
|
||||
Linux::KeyUndo => Err(()),
|
||||
Linux::KeyFront => Err(()),
|
||||
Linux::KeyCopy => Err(()),
|
||||
Linux::KeyOpen => Err(()),
|
||||
Linux::KeyPaste => Err(()),
|
||||
Linux::KeyFind => Ok(Self::ACSearch),
|
||||
Linux::KeyCut => Err(()),
|
||||
Linux::KeyHelp => Ok(Self::KeyF1), // AL Integrated Help Center?
|
||||
Linux::KeyMenu => Ok(Self::KeyApplication),
|
||||
Linux::KeyCalc => Ok(Self::ALCalculator),
|
||||
Linux::KeySetup => Err(()),
|
||||
Linux::KeySleep => Ok(Self::SystemSleep),
|
||||
Linux::KeyWakeup => Ok(Self::SystemWakeUp),
|
||||
Linux::KeyFile => Ok(Self::ALLocalMachineBrowser),
|
||||
Linux::KeySendfile => Err(()),
|
||||
Linux::KeyDeletefile => Err(()),
|
||||
Linux::KeyXfer => Err(()),
|
||||
Linux::KeyProg1 => Err(()),
|
||||
Linux::KeyProg2 => Err(()),
|
||||
Linux::KeyWww => Ok(Self::ACSearch), // TODO unsure
|
||||
Linux::KeyMsdos => Err(()),
|
||||
Linux::KeyCoffee => Err(()),
|
||||
Linux::KeyRotateDisplay => Err(()),
|
||||
Linux::KeyCyclewindows => Err(()),
|
||||
Linux::KeyMail => Ok(Self::ALEmailReader),
|
||||
Linux::KeyBookmarks => Ok(Self::ACBookmarks),
|
||||
Linux::KeyComputer => Ok(Self::ACHome),
|
||||
Linux::KeyBack => Ok(Self::ACBack),
|
||||
Linux::KeyForward => Ok(Self::ACForward),
|
||||
Linux::KeyClosecd => Err(()),
|
||||
Linux::KeyEjectcd => Err(()),
|
||||
Linux::KeyEjectclosecd => Err(()),
|
||||
Linux::KeyNextsong => Ok(Self::KeyScanNextTrack),
|
||||
Linux::KeyPlaypause => Ok(Self::KeyPlayPause),
|
||||
Linux::KeyPrevioussong => Ok(Self::KeyScanPreviousTrack),
|
||||
Linux::KeyStopcd => Ok(Self::KeyStop),
|
||||
Linux::KeyRecord => Err(()),
|
||||
Linux::KeyRewind => Err(()),
|
||||
Linux::KeyPhone => Err(()),
|
||||
Linux::KeyIso => Err(()),
|
||||
Linux::KeyConfig => Err(()),
|
||||
Linux::KeyHomepage => Ok(Self::ACHome),
|
||||
Linux::KeyRefresh => Ok(Self::ACRefresh),
|
||||
Linux::KeyExit => Err(()),
|
||||
Linux::KeyMove => Err(()),
|
||||
Linux::KeyEdit => Err(()),
|
||||
Linux::KeyScrollup => Err(()),
|
||||
Linux::KeyScrolldown => Err(()),
|
||||
Linux::KeyKpleftparen => Err(()),
|
||||
Linux::KeyKprightparen => Err(()),
|
||||
Linux::KeyNew => Err(()),
|
||||
Linux::KeyRedo => Err(()),
|
||||
Linux::KeyF13 => Ok(Self::KeyF13),
|
||||
Linux::KeyF14 => Ok(Self::KeyF14),
|
||||
Linux::KeyF15 => Ok(Self::KeyF15),
|
||||
Linux::KeyF16 => Ok(Self::KeyF16),
|
||||
Linux::KeyF17 => Ok(Self::KeyF17),
|
||||
Linux::KeyF18 => Ok(Self::KeyF18),
|
||||
Linux::KeyF19 => Ok(Self::KeyF19),
|
||||
Linux::KeyF20 => Ok(Self::KeyF20),
|
||||
Linux::KeyF21 => Ok(Self::KeyF21),
|
||||
Linux::KeyF22 => Ok(Self::KeyF22),
|
||||
Linux::KeyF23 => Ok(Self::KeyF23),
|
||||
Linux::KeyF24 => Ok(Self::KeyF24),
|
||||
Linux::KeyPlaycd => Err(()),
|
||||
Linux::KeyPausecd => Err(()),
|
||||
Linux::KeyProg3 => Err(()),
|
||||
Linux::KeyProg4 => Err(()),
|
||||
Linux::KeyAllApplications => Err(()),
|
||||
Linux::KeySuspend => Err(()),
|
||||
Linux::KeyClose => Err(()),
|
||||
Linux::KeyPlay => Err(()),
|
||||
Linux::KeyFastforward => Err(()),
|
||||
Linux::KeyBassboost => Err(()),
|
||||
Linux::KeyPrint => Err(()),
|
||||
Linux::KeyHp => Err(()),
|
||||
Linux::KeyCamera => Err(()),
|
||||
Linux::KeySound => Err(()),
|
||||
Linux::KeyQuestion => Err(()),
|
||||
Linux::KeyEmail => Err(()),
|
||||
Linux::KeyChat => Err(()),
|
||||
Linux::KeySearch => Err(()),
|
||||
Linux::KeyConnect => Err(()),
|
||||
Linux::KeyFinance => Err(()),
|
||||
Linux::KeySport => Err(()),
|
||||
Linux::KeyShop => Err(()),
|
||||
Linux::KeyAlterase => Err(()),
|
||||
Linux::KeyCancel => Err(()),
|
||||
Linux::KeyBrightnessdown => Err(()),
|
||||
Linux::KeyBrightnessup => Err(()),
|
||||
Linux::KeyMedia => Err(()),
|
||||
Linux::KeySwitchvideomode => Err(()),
|
||||
Linux::KeyKbdillumtoggle => Err(()),
|
||||
Linux::KeyKbdillumdown => Err(()),
|
||||
Linux::KeyKbdillumup => Err(()),
|
||||
Linux::KeySend => Err(()),
|
||||
Linux::KeyReply => Err(()),
|
||||
Linux::KeyForwardmail => Err(()),
|
||||
Linux::KeySave => Err(()),
|
||||
Linux::KeyDocuments => Err(()),
|
||||
Linux::KeyBattery => Err(()),
|
||||
Linux::KeyBluetooth => Err(()),
|
||||
Linux::KeyWlan => Err(()),
|
||||
Linux::KeyUwb => Err(()),
|
||||
Linux::KeyUnknown => Err(()),
|
||||
Linux::KeyVideoNext => Err(()),
|
||||
Linux::KeyVideoPrev => Err(()),
|
||||
Linux::KeyBrightnessCycle => Err(()),
|
||||
Linux::KeyBrightnessAuto => Err(()),
|
||||
Linux::KeyDisplayOff => Err(()),
|
||||
Linux::KeyWwan => Err(()),
|
||||
Linux::KeyRfkill => Err(()),
|
||||
Linux::KeyMicmute => Err(()),
|
||||
Linux::KeyCount => Err(()),
|
||||
Linux::Invalid => Err(()),
|
||||
Linux::Invalid1 => Err(()),
|
||||
Linux::Invalid2 => Err(()),
|
||||
Linux::Invalid3 => Err(()),
|
||||
Linux::Invalid4 => Err(()),
|
||||
Linux::Invalid5 => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
860
src/server.rs
Normal file
860
src/server.rs
Normal file
@@ -0,0 +1,860 @@
|
||||
use anyhow::anyhow;
|
||||
use futures::stream::StreamExt;
|
||||
use log;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
collections::HashSet,
|
||||
error::Error,
|
||||
io::Result,
|
||||
net::IpAddr,
|
||||
rc::Rc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::{io::ReadHalf, net::UdpSocket, signal, sync::mpsc::Sender, task};
|
||||
|
||||
#[cfg(unix)]
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
#[cfg(windows)]
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use std::{io::ErrorKind, net::SocketAddr};
|
||||
|
||||
use crate::{
|
||||
client::{ClientEvent, ClientHandle, ClientManager, Position},
|
||||
config::Config,
|
||||
consumer::EventConsumer,
|
||||
dns,
|
||||
frontend::{self, FrontendEvent, FrontendListener, FrontendNotify},
|
||||
producer::EventProducer,
|
||||
};
|
||||
use crate::{
|
||||
consumer,
|
||||
event::{Event, KeyboardEvent},
|
||||
producer,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum State {
|
||||
Sending,
|
||||
Receiving,
|
||||
AwaitingLeave,
|
||||
}
|
||||
|
||||
pub enum ProducerEvent {
|
||||
Release,
|
||||
ClientEvent(ClientEvent),
|
||||
}
|
||||
|
||||
pub enum ConsumerEvent {
|
||||
ClientEvent(ClientEvent),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ClientUpdate {
|
||||
client: ClientHandle,
|
||||
hostname: Option<String>,
|
||||
port: u16,
|
||||
pos: Position,
|
||||
}
|
||||
|
||||
pub struct Server {}
|
||||
|
||||
impl Server {
|
||||
pub async fn run(config: &Config) -> anyhow::Result<()> {
|
||||
// create frontend communication adapter
|
||||
let mut frontend = match FrontendListener::new().await {
|
||||
Some(Err(e)) => return Err(e),
|
||||
Some(Ok(f)) => f,
|
||||
None => {
|
||||
// none means some other instance is already running
|
||||
log::info!("service already running, exiting");
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
};
|
||||
let (mut consumer, mut producer) = tokio::join!(consumer::create(), producer::create());
|
||||
|
||||
// create dns resolver
|
||||
let resolver = dns::DnsResolver::new().await?;
|
||||
|
||||
// bind the udp socket
|
||||
let listen_addr = SocketAddr::new("0.0.0.0".parse().unwrap(), config.port);
|
||||
let mut socket = UdpSocket::bind(listen_addr).await?;
|
||||
let (frontend_tx, mut frontend_rx) = tokio::sync::mpsc::channel(1);
|
||||
|
||||
// create client manager
|
||||
let client_manager_rc = Rc::new(RefCell::new(ClientManager::new()));
|
||||
|
||||
let state_rc = Rc::new(Cell::new(State::Receiving));
|
||||
|
||||
// channel to notify producer
|
||||
let (producer_notify_tx, mut producer_notify_rx) = tokio::sync::mpsc::channel(32);
|
||||
|
||||
// channel to notify consumer
|
||||
let (consumer_notify_tx, mut consumer_notify_rx) = tokio::sync::mpsc::channel(32);
|
||||
|
||||
// channel to request dns resolver
|
||||
let (resolve_tx, mut resolve_rx) = tokio::sync::mpsc::channel(32);
|
||||
|
||||
// channel to send events to frontends
|
||||
let (frontend_notify_tx, mut frontend_notify_rx) = tokio::sync::mpsc::channel(32);
|
||||
|
||||
// channels for udp send / receive
|
||||
let (receiver_tx, mut 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);
|
||||
|
||||
// add clients from config
|
||||
for (c, h, port, p) in config.get_clients().into_iter() {
|
||||
Self::add_client(
|
||||
&resolve_tx,
|
||||
&client_manager_rc,
|
||||
&mut frontend,
|
||||
h,
|
||||
c,
|
||||
port,
|
||||
p,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// event producer
|
||||
let client_manager = client_manager_rc.clone();
|
||||
let state = state_rc.clone();
|
||||
let sender_ch = sender_tx.clone();
|
||||
let producer_task = tokio::task::spawn_local(async move {
|
||||
loop {
|
||||
tokio::select! {
|
||||
e = producer.next() => {
|
||||
let (client, event) = match e {
|
||||
Some(e) => e?,
|
||||
None => return Err::<(), anyhow::Error>(anyhow!("event producer closed")),
|
||||
};
|
||||
Self::handle_producer_event(&mut producer, &client_manager, &state, &sender_ch, client, event).await;
|
||||
}
|
||||
e = producer_notify_rx.recv() => {
|
||||
match e {
|
||||
Some(e) => match e {
|
||||
ProducerEvent::Release => producer.release(),
|
||||
ProducerEvent::ClientEvent(e) => producer.notify(e),
|
||||
},
|
||||
None => break Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// event consumer
|
||||
let client_manager = client_manager_rc.clone();
|
||||
let state = state_rc.clone();
|
||||
let producer_notify = producer_notify_tx.clone();
|
||||
let receiver_task = tokio::task::spawn_local(async move {
|
||||
let mut last_ignored = None;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
udp_event = receiver_rx.recv() => {
|
||||
let udp_event = match udp_event {
|
||||
Some(Ok(e)) => e,
|
||||
Some(Err(e)) => return Err::<(), anyhow::Error>(anyhow!("{}", e)),
|
||||
None => return Err::<(), anyhow::Error>(anyhow!("receiver closed")),
|
||||
};
|
||||
Self::handle_udp_rx(&client_manager, &producer_notify, &mut consumer, &sender_tx, &state, &mut last_ignored, udp_event).await;
|
||||
}
|
||||
consumer_event = consumer_notify_rx.recv() => {
|
||||
match consumer_event {
|
||||
Some(e) => match e {
|
||||
ConsumerEvent::ClientEvent(e) => consumer.notify(e).await,
|
||||
},
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
_ = consumer.dispatch() => { }
|
||||
}
|
||||
}
|
||||
// destroy consumer
|
||||
consumer.destroy().await;
|
||||
Ok(())
|
||||
});
|
||||
|
||||
// frontend listener
|
||||
let client_manager = client_manager_rc.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;
|
||||
}
|
||||
};
|
||||
Self::handle_frontend_stream(&client_manager, &mut frontend, &frontend_tx, stream).await;
|
||||
}
|
||||
event = frontend_rx.recv() => {
|
||||
let frontend_event = match event {
|
||||
Some(e) => e,
|
||||
None => return Err::<(), anyhow::Error>(anyhow!("frontend channel closed")),
|
||||
};
|
||||
let exit = Self::handle_frontend_event(&producer_notify_tx, &consumer_notify_tx, &client_manager, &resolve_tx, &mut frontend, &port_tx, frontend_event).await;
|
||||
if exit {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
notify = frontend_notify_rx.recv() => {
|
||||
let notify = match notify {
|
||||
Some(n) => n,
|
||||
None => return Err::<(), anyhow::Error>(anyhow!("frontend notify closed")),
|
||||
};
|
||||
let _ = frontend.notify_all(notify).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// dns resolver
|
||||
let client_manager = client_manager_rc.clone();
|
||||
let resolver_task = tokio::task::spawn_local(async move {
|
||||
loop {
|
||||
let (host, client): (String, ClientHandle) = match resolve_rx.recv().await {
|
||||
Some(r) => r,
|
||||
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) = client_manager.borrow_mut().get_mut(client) {
|
||||
let port = state.client.port;
|
||||
let mut addrs = HashSet::from_iter(
|
||||
state
|
||||
.client
|
||||
.fix_ips
|
||||
.iter()
|
||||
.map(|a| SocketAddr::new(*a, port)),
|
||||
);
|
||||
for ip in ips {
|
||||
let sock_addr = SocketAddr::new(ip, port);
|
||||
addrs.insert(sock_addr);
|
||||
}
|
||||
state.client.addrs = addrs;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// udp task
|
||||
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;
|
||||
};
|
||||
let current_port = socket.local_addr().unwrap().port();
|
||||
if current_port == port {
|
||||
let _ = frontend_notify_tx.send(FrontendNotify::NotifyPortChange(port, None)).await;
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let reaper = task::spawn_local(async move {
|
||||
tokio::select! {
|
||||
_ = signal::ctrl_c() => {
|
||||
log::info!("terminating service");
|
||||
},
|
||||
_ = producer_task => {
|
||||
// TODO restart producer?
|
||||
}
|
||||
_ = receiver_task => {
|
||||
// TODO restart producer?
|
||||
}
|
||||
_ = frontend_task => {
|
||||
// frontend exited => exit requested
|
||||
}
|
||||
_ = resolver_task => {
|
||||
// resolver exited
|
||||
}
|
||||
_ = udp_task => {
|
||||
// udp exited
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
reaper.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_client(
|
||||
resolver_tx: &Sender<(String, ClientHandle)>,
|
||||
client_manager: &Rc<RefCell<ClientManager>>,
|
||||
frontend: &mut FrontendListener,
|
||||
hostname: Option<String>,
|
||||
addr: HashSet<IpAddr>,
|
||||
port: u16,
|
||||
pos: Position,
|
||||
) -> ClientHandle {
|
||||
log::info!(
|
||||
"adding client [{}]{} @ {:?}",
|
||||
pos,
|
||||
hostname.as_deref().unwrap_or(""),
|
||||
&addr
|
||||
);
|
||||
let client = client_manager
|
||||
.borrow_mut()
|
||||
.add_client(hostname.clone(), addr, port, pos);
|
||||
|
||||
log::debug!("add_client {client}");
|
||||
if let Some(hostname) = hostname.clone() {
|
||||
let _ = resolver_tx.send((hostname, client)).await;
|
||||
};
|
||||
let notify = FrontendNotify::NotifyClientCreate(client, hostname, port, pos);
|
||||
if let Err(e) = frontend.notify_all(notify).await {
|
||||
log::error!("error notifying frontend: {e}");
|
||||
};
|
||||
client
|
||||
}
|
||||
|
||||
pub async fn activate_client(
|
||||
producer_notify_tx: &Sender<ProducerEvent>,
|
||||
consumer_notify_tx: &Sender<ConsumerEvent>,
|
||||
client_manager: &Rc<RefCell<ClientManager>>,
|
||||
client: ClientHandle,
|
||||
active: bool,
|
||||
) {
|
||||
let (client, pos) = match 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(
|
||||
client_manager: &Rc<RefCell<ClientManager>>,
|
||||
producer_notify_tx: &Sender<ProducerEvent>,
|
||||
consumer_notify_tx: &Sender<ConsumerEvent>,
|
||||
frontend: &mut FrontendListener,
|
||||
client: ClientHandle,
|
||||
) -> Option<ClientHandle> {
|
||||
let _ = producer_notify_tx
|
||||
.send(ProducerEvent::ClientEvent(ClientEvent::Destroy(client)))
|
||||
.await;
|
||||
let _ = consumer_notify_tx
|
||||
.send(ConsumerEvent::ClientEvent(ClientEvent::Destroy(client)))
|
||||
.await;
|
||||
|
||||
let Some(client) = client_manager
|
||||
.borrow_mut()
|
||||
.remove_client(client)
|
||||
.map(|s| s.client.handle)
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
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(
|
||||
producer_notify_tx: &Sender<ProducerEvent>,
|
||||
consumer_notify_tx: &Sender<ConsumerEvent>,
|
||||
resolve_tx: &Sender<(String, ClientHandle)>,
|
||||
client_manager: &Rc<RefCell<ClientManager>>,
|
||||
client_update: ClientUpdate,
|
||||
) {
|
||||
let (hostname, handle, active) = {
|
||||
// retrieve state
|
||||
let mut client_manager = client_manager.borrow_mut();
|
||||
let Some(state) = client_manager.get_mut(client_update.client) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// update pos
|
||||
state.client.pos = client_update.pos;
|
||||
|
||||
// update port
|
||||
if state.client.port != client_update.port {
|
||||
state.client.port = client_update.port;
|
||||
state.client.addrs = state
|
||||
.client
|
||||
.addrs
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|mut a| {
|
||||
a.set_port(client_update.port);
|
||||
a
|
||||
})
|
||||
.collect();
|
||||
state
|
||||
.client
|
||||
.active_addr
|
||||
.map(|a| SocketAddr::new(a.ip(), client_update.port));
|
||||
}
|
||||
|
||||
// update hostname
|
||||
if state.client.hostname != client_update.hostname {
|
||||
state.client.addrs = HashSet::new();
|
||||
state.client.active_addr = None;
|
||||
state.client.hostname = client_update.hostname;
|
||||
}
|
||||
|
||||
log::debug!("client updated: {:?}", state);
|
||||
(
|
||||
state.client.hostname.clone(),
|
||||
state.client.handle,
|
||||
state.active,
|
||||
)
|
||||
};
|
||||
|
||||
// resolve dns
|
||||
if let Some(hostname) = hostname {
|
||||
let _ = resolve_tx.send((hostname, handle)).await;
|
||||
}
|
||||
|
||||
// update state in event consumer & producer
|
||||
if active {
|
||||
let _ = producer_notify_tx
|
||||
.send(ProducerEvent::ClientEvent(ClientEvent::Destroy(
|
||||
client_update.client,
|
||||
)))
|
||||
.await;
|
||||
let _ = consumer_notify_tx
|
||||
.send(ConsumerEvent::ClientEvent(ClientEvent::Destroy(
|
||||
client_update.client,
|
||||
)))
|
||||
.await;
|
||||
let _ = producer_notify_tx
|
||||
.send(ProducerEvent::ClientEvent(ClientEvent::Create(
|
||||
client_update.client,
|
||||
client_update.pos,
|
||||
)))
|
||||
.await;
|
||||
let _ = consumer_notify_tx
|
||||
.send(ConsumerEvent::ClientEvent(ClientEvent::Create(
|
||||
client_update.client,
|
||||
client_update.pos,
|
||||
)))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_udp_rx(
|
||||
client_manager: &Rc<RefCell<ClientManager>>,
|
||||
producer_notify_tx: &Sender<ProducerEvent>,
|
||||
consumer: &mut Box<dyn EventConsumer>,
|
||||
sender_tx: &Sender<(Event, SocketAddr)>,
|
||||
state: &Rc<Cell<State>>,
|
||||
last_ignored: &mut Option<SocketAddr>,
|
||||
event: (Event, SocketAddr),
|
||||
) {
|
||||
let (event, addr) = event;
|
||||
|
||||
// get handle for addr
|
||||
let handle = match 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 = 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.last_seen = Some(Instant::now());
|
||||
// set addr as new default for this client
|
||||
client_state.client.active_addr = Some(addr);
|
||||
}
|
||||
|
||||
match (event, addr) {
|
||||
(Event::Pong(), _) => { /* ignore pong events */ }
|
||||
(Event::Ping(), addr) => {
|
||||
let _ = sender_tx.send((Event::Pong(), addr)).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 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
|
||||
let _ = producer_notify_tx.send(ProducerEvent::Release).await;
|
||||
state.replace(State::Receiving);
|
||||
log::trace!("STATE ===> Receiving");
|
||||
}
|
||||
}
|
||||
State::Receiving => {
|
||||
// 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 {
|
||||
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 {
|
||||
state.replace(State::Receiving);
|
||||
log::trace!("STATE ===> Receiving");
|
||||
let _ = producer_notify_tx.send(ProducerEvent::Release).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let pong = {
|
||||
let mut client_manager = client_manager.borrow_mut();
|
||||
let client_state = match client_manager.get_mut(handle) {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
log::error!("unknown handle");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// let the server know we are still alive once every second
|
||||
if client_state.last_replied.is_none()
|
||||
|| client_state.last_replied.is_some()
|
||||
&& client_state.last_replied.unwrap().elapsed() > Duration::from_secs(1)
|
||||
{
|
||||
client_state.last_replied = Some(Instant::now());
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if pong {
|
||||
let _ = sender_tx.send((Event::Pong(), addr)).await;
|
||||
}
|
||||
}
|
||||
|
||||
const RELEASE_MODIFIERDS: u32 = 77; // ctrl+shift+super+alt
|
||||
|
||||
async fn handle_producer_event(
|
||||
producer: &mut Box<dyn EventProducer>,
|
||||
client_manager: &Rc<RefCell<ClientManager>>,
|
||||
state: &Rc<Cell<State>>,
|
||||
sender_tx: &Sender<(Event, SocketAddr)>,
|
||||
c: ClientHandle,
|
||||
mut e: Event,
|
||||
) {
|
||||
log::trace!("producer: ({c}) {e:?}");
|
||||
|
||||
if let Event::Keyboard(crate::event::KeyboardEvent::Modifiers {
|
||||
mods_depressed,
|
||||
mods_latched: _,
|
||||
mods_locked: _,
|
||||
group: _,
|
||||
}) = e
|
||||
{
|
||||
if mods_depressed == Self::RELEASE_MODIFIERDS {
|
||||
producer.release();
|
||||
state.replace(State::Receiving);
|
||||
log::trace!("STATE ===> Receiving");
|
||||
// send an event to release all the modifiers
|
||||
e = Event::Keyboard(KeyboardEvent::Modifiers {
|
||||
mods_depressed: 0,
|
||||
mods_latched: 0,
|
||||
mods_locked: 0,
|
||||
group: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let (addr, enter, ping_addrs) = {
|
||||
let mut enter = false;
|
||||
let mut ping_addrs: Option<Vec<SocketAddr>> = None;
|
||||
|
||||
// get client state for handle
|
||||
let mut client_manager = 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();
|
||||
state.replace(State::Receiving);
|
||||
log::trace!("STATE ===> Receiving");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// if we just entered the client we want to send additional enter events until
|
||||
// we get a leave event
|
||||
if let State::Receiving | State::AwaitingLeave = state.get() {
|
||||
state.replace(State::AwaitingLeave);
|
||||
log::trace!("STATE ===> AwaitingLeave");
|
||||
enter = true;
|
||||
}
|
||||
|
||||
let last_seen = match client_state.last_seen {
|
||||
None => Duration::MAX,
|
||||
Some(i) => i.elapsed(),
|
||||
};
|
||||
|
||||
let last_pinged = match client_state.last_ping {
|
||||
None => Duration::MAX,
|
||||
Some(i) => i.elapsed(),
|
||||
};
|
||||
|
||||
// not seen for one second but pinged at least 500ms ago
|
||||
if last_seen > Duration::from_secs(1)
|
||||
&& last_pinged > Duration::from_millis(500)
|
||||
&& last_pinged < Duration::from_secs(1)
|
||||
{
|
||||
// client unresponsive -> set state to receiving
|
||||
if state.get() != State::Receiving {
|
||||
log::info!("client not responding - releasing pointer");
|
||||
producer.release();
|
||||
state.replace(State::Receiving);
|
||||
log::trace!("STATE ===> Receiving");
|
||||
}
|
||||
}
|
||||
|
||||
// last ping > 500ms ago -> ping all interfaces
|
||||
if last_pinged > Duration::from_millis(500) {
|
||||
ping_addrs = Some(client_state.client.addrs.iter().cloned().collect());
|
||||
client_state.last_ping = Some(Instant::now());
|
||||
}
|
||||
|
||||
(client_state.client.active_addr, enter, ping_addrs)
|
||||
};
|
||||
if let Some(addr) = addr {
|
||||
if enter {
|
||||
let _ = sender_tx.send((Event::Enter(), addr)).await;
|
||||
}
|
||||
let _ = sender_tx.send((e, addr)).await;
|
||||
}
|
||||
if let Some(addrs) = ping_addrs {
|
||||
for addr in addrs {
|
||||
let _ = sender_tx.send((Event::Ping(), addr)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_frontend_stream(
|
||||
client_manager: &Rc<RefCell<ClientManager>>,
|
||||
frontend: &mut FrontendListener,
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Self::enumerate(client_manager, frontend).await;
|
||||
}
|
||||
|
||||
async fn handle_frontend_event(
|
||||
producer_notify_tx: &Sender<ProducerEvent>,
|
||||
consumer_notify_tx: &Sender<ConsumerEvent>,
|
||||
client_manager: &Rc<RefCell<ClientManager>>,
|
||||
resolve_tx: &Sender<(String, ClientHandle)>,
|
||||
frontend: &mut FrontendListener,
|
||||
port_tx: &Sender<u16>,
|
||||
event: FrontendEvent,
|
||||
) -> bool {
|
||||
log::debug!("frontend: {event:?}");
|
||||
match event {
|
||||
FrontendEvent::AddClient(hostname, port, pos) => {
|
||||
Self::add_client(
|
||||
resolve_tx,
|
||||
client_manager,
|
||||
frontend,
|
||||
hostname,
|
||||
HashSet::new(),
|
||||
port,
|
||||
pos,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
FrontendEvent::ActivateClient(client, active) => {
|
||||
Self::activate_client(
|
||||
producer_notify_tx,
|
||||
consumer_notify_tx,
|
||||
client_manager,
|
||||
client,
|
||||
active,
|
||||
)
|
||||
.await
|
||||
}
|
||||
FrontendEvent::ChangePort(port) => {
|
||||
let _ = port_tx.send(port).await;
|
||||
}
|
||||
FrontendEvent::DelClient(client) => {
|
||||
Self::remove_client(
|
||||
client_manager,
|
||||
producer_notify_tx,
|
||||
consumer_notify_tx,
|
||||
frontend,
|
||||
client,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
FrontendEvent::Enumerate() => Self::enumerate(client_manager, frontend).await,
|
||||
FrontendEvent::Shutdown() => {
|
||||
log::info!("terminating gracefully...");
|
||||
return true;
|
||||
}
|
||||
FrontendEvent::UpdateClient(client, hostname, port, pos) => {
|
||||
let client_update = ClientUpdate {
|
||||
client,
|
||||
hostname,
|
||||
port,
|
||||
pos,
|
||||
};
|
||||
Self::update_client(
|
||||
producer_notify_tx,
|
||||
consumer_notify_tx,
|
||||
resolve_tx,
|
||||
client_manager,
|
||||
client_update,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
async fn enumerate(
|
||||
client_manager: &Rc<RefCell<ClientManager>>,
|
||||
frontend: &mut FrontendListener,
|
||||
) {
|
||||
let clients = client_manager
|
||||
.borrow()
|
||||
.get_client_states()
|
||||
.map(|s| (s.client.clone(), s.active))
|
||||
.collect();
|
||||
if let Err(e) = frontend
|
||||
.notify_all(FrontendNotify::Enumerate(clients))
|
||||
.await
|
||||
{
|
||||
log::error!("error notifying frontend: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn receive_event(
|
||||
socket: &UdpSocket,
|
||||
) -> std::result::Result<(Event, SocketAddr), Box<dyn Error>> {
|
||||
let mut buf = vec![0u8; 22];
|
||||
match socket.recv_from(&mut buf).await {
|
||||
Ok((_amt, src)) => Ok((Event::try_from(buf)?, src)),
|
||||
Err(e) => Err(Box::new(e)),
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
sock.try_send_to(&data, addr)
|
||||
}
|
||||
Reference in New Issue
Block a user