mirror of
https://github.com/feschber/lan-mouse.git
synced 2026-03-08 12:30:00 +03:00
Compare commits
125 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5415205c83 | ||
|
|
1666fb8b7b | ||
|
|
9afe7da0dd | ||
|
|
f7c59e40c9 | ||
|
|
5be5b0ad7c | ||
|
|
8ed4520172 | ||
|
|
9e56c546cd | ||
|
|
daf8818a9f | ||
|
|
6eaa199503 | ||
|
|
8ff991aefe | ||
|
|
abf95afb9f | ||
|
|
9a75a7622e | ||
|
|
0196cfe56c | ||
|
|
097468f708 | ||
|
|
3470abc03a | ||
|
|
a7397ad4f4 | ||
|
|
9889b49f10 | ||
|
|
f4db2366b7 | ||
|
|
c9deb6eba4 | ||
|
|
5cc8cda19d | ||
|
|
8084b52cfc | ||
|
|
1f4821a16d | ||
|
|
82926d8272 | ||
|
|
006831b9f1 | ||
|
|
e5b770a799 | ||
|
|
017bc43176 | ||
|
|
36001c6fb2 | ||
|
|
2803db7073 | ||
|
|
622b04b36c | ||
|
|
61ff05c95a | ||
|
|
ecab3a360d | ||
|
|
6674af8e63 | ||
|
|
b3caba99ab | ||
|
|
fad48c2504 | ||
|
|
f28f75418c | ||
|
|
e2c47d3096 | ||
|
|
f19944515a | ||
|
|
535cd055b9 | ||
|
|
118c0dfc73 | ||
|
|
7897db6047 | ||
|
|
347256e966 | ||
|
|
0017dbc634 | ||
|
|
d90eb0cd0f | ||
|
|
2e52660714 | ||
|
|
8c276f88b7 | ||
|
|
13597b3587 | ||
|
|
b59808742a | ||
|
|
6c99f9bea3 | ||
|
|
d54b3a08e1 | ||
|
|
767fc8bd6b | ||
|
|
fa15048ad8 | ||
|
|
eb366bcd34 | ||
|
|
91176b1267 | ||
|
|
a6f386ea83 | ||
|
|
40b0cdd52e | ||
|
|
1553ed4212 | ||
|
|
4561c20610 | ||
|
|
6cdb607b11 | ||
|
|
f5827bb31c | ||
|
|
64e3bf3ff4 | ||
|
|
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 |
24
.github/workflows/cachix.yml
vendored
Normal file
24
.github/workflows/cachix.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Binary Cache
|
||||
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
jobs:
|
||||
nix:
|
||||
name: "Build"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
logger: pretty
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
- uses: cachix/cachix-action@v14
|
||||
with:
|
||||
name: lan-mouse
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
|
||||
- name: Build lan-mouse
|
||||
run: nix build --print-build-logs
|
||||
69
.github/workflows/pre-release.yml
vendored
69
.github/workflows/pre-release.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
linux-release-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
- name: Release Build
|
||||
run: cargo build --release
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lan-mouse-linux
|
||||
path: target/release/lan-mouse
|
||||
@@ -30,18 +30,72 @@ jobs:
|
||||
windows-release-build:
|
||||
runs-on: windows-latest
|
||||
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"
|
||||
- uses: actions/checkout@v4
|
||||
- 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
|
||||
uses: actions/upload-artifact@v4
|
||||
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@v4
|
||||
- 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@v4
|
||||
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
|
||||
@@ -55,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
|
||||
|
||||
74
.github/workflows/rust.yml
vendored
74
.github/workflows/rust.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -25,8 +25,12 @@ jobs:
|
||||
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
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lan-mouse
|
||||
path: target/debug/lan-mouse
|
||||
@@ -36,13 +40,73 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- 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
|
||||
uses: actions/upload-artifact@v4
|
||||
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@v4
|
||||
- name: install dependencies
|
||||
run: brew install gtk4 libadwaita
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
- name: Check Formatting
|
||||
run: cargo fmt --check
|
||||
- name: Clippy
|
||||
run: cargo clippy --all-features --all-targets -- --deny warnings
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lan-mouse-macos
|
||||
path: target/debug/lan-mouse
|
||||
|
||||
69
.github/workflows/tagged-release.yml
vendored
69
.github/workflows/tagged-release.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
linux-release-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
- name: Release Build
|
||||
run: cargo build --release
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lan-mouse-linux
|
||||
path: target/release/lan-mouse
|
||||
@@ -26,18 +26,72 @@ jobs:
|
||||
windows-release-build:
|
||||
runs-on: windows-latest
|
||||
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"
|
||||
- uses: actions/checkout@v4
|
||||
- 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
|
||||
uses: actions/upload-artifact@v4
|
||||
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@v4
|
||||
- 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@v4
|
||||
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
|
||||
@@ -49,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
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1 +1,7 @@
|
||||
/target
|
||||
.gdbinit
|
||||
.idea/
|
||||
.vs/
|
||||
.vscode/
|
||||
.direnv/
|
||||
result
|
||||
1535
Cargo.lock
generated
1535
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
61
Cargo.toml
61
Cargo.toml
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "lan-mouse"
|
||||
description = "Software KVM Switch / mouse & keyboard sharing software for Local Area Networks"
|
||||
version = "0.3.3"
|
||||
version = "0.7.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
repository = "https://github.com/ferdinandschober/lan-mouse"
|
||||
@@ -16,47 +16,48 @@ lto = "fat"
|
||||
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 }
|
||||
keycode = "0.4.0"
|
||||
once_cell = "1.19.0"
|
||||
|
||||
[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_6"], optional = true }
|
||||
adw = { package = "libadwaita", version = "0.5.2", features = ["v1_1"], optional = true }
|
||||
ashpd = { version = "0.8", 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"] }
|
||||
|
||||
[target.'cfg(unix)'.build-dependencies]
|
||||
[build-dependencies]
|
||||
glib-build-tools = "0.18.0"
|
||||
|
||||
[features]
|
||||
default = [
|
||||
"wayland",
|
||||
"x11",
|
||||
"xdg_desktop_portal",
|
||||
"libei",
|
||||
"gtk",
|
||||
]
|
||||
wayland = [
|
||||
"dep:wayland-client",
|
||||
"dep:wayland-protocols",
|
||||
"dep:wayland-protocols-wlr",
|
||||
"dep:wayland-protocols-misc",
|
||||
"dep:wayland-protocols-plasma" ]
|
||||
x11 = [ "dep:x11" ]
|
||||
xdg_desktop_portal = []
|
||||
libei = []
|
||||
gtk = ["dep:gtk", "dep:adw"]
|
||||
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 = ["dep:ashpd"]
|
||||
libei = ["dep:reis", "dep:ashpd"]
|
||||
gtk = ["dep:gtk", "dep:adw", "dep:async-channel"]
|
||||
|
||||
440
README.md
440
README.md
@@ -1,57 +1,86 @@
|
||||
# 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/).
|
||||
Goal of this project is to be an open-source replacement for proprietary tools like [Synergy 2/3](https://symless.com/synergy), [Share Mouse](https://www.sharemouse.com/de/).
|
||||
|
||||
Focus lies on performance and a clean, manageable implementation that can easily be expanded to support additional backends like e.g. Android, iOS, ... .
|
||||
|
||||
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).
|
||||
|
||||
_Now with a gtk frontend_
|
||||
|
||||
## Configuration
|
||||
Configuration is done through the file `config.toml`,
|
||||
which must be located in the current working directory when
|
||||
executing lan-mouse.
|
||||
> [!WARNING]
|
||||
> Since this tool has gained a bit of popularity over the past couple of days:
|
||||
>
|
||||
> All network traffic is currently **unencrypted** and sent in **plaintext**.
|
||||
>
|
||||
> A malicious actor with access to the network could read input data or send input events with spoofed IPs to take control over a device.
|
||||
>
|
||||
> Therefore you should only use this tool in your local network with trusted devices for now
|
||||
> and I take no responsibility for any leakage of data!
|
||||
|
||||
### Example config
|
||||
A config file could look like this:
|
||||
|
||||
```toml
|
||||
# example configuration
|
||||
## OS Support
|
||||
|
||||
# optional port (defaults to 4242)
|
||||
port = 4242
|
||||
# # optional frontend -> defaults to gtk if available
|
||||
# # possible values are "cli" and "gtk"
|
||||
# frontend = "gtk"
|
||||
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:
|
||||
|
||||
# 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"]
|
||||
| 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: | :heavy_check_mark: |
|
||||
| X11 | :heavy_check_mark: | WIP |
|
||||
| Windows | :heavy_check_mark: | WIP |
|
||||
| MacOS | :heavy_check_mark: | WIP |
|
||||
|
||||
# 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
|
||||
> [!Important]
|
||||
> Gnome -> Sway only partially works (modifier events are not handled correctly)
|
||||
|
||||
> [!Important]
|
||||
> **Wayfire**
|
||||
>
|
||||
> If you are using [Wayfire](https://github.com/WayfireWM/wayfire), make sure to use a recent version (must be newer than October 23rd) and **add `shortcuts-inhibit` to the list of plugins in your wayfire config!**
|
||||
> Otherwise input capture will not work.
|
||||
|
||||
## Installation
|
||||
|
||||
### Download from Releases
|
||||
The easiest way to install Lan Mouse is to download precompiled release binaries from the [releases section](https://github.com/feschber/lan-mouse/releases).
|
||||
|
||||
For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies).
|
||||
|
||||
### Arch Linux
|
||||
Lan Mouse is available on the AUR:
|
||||
```sh
|
||||
# git version (includes latest changes)
|
||||
paru -S lan-mouse-git
|
||||
|
||||
# alternatively
|
||||
paru -S lan-mouse-bin
|
||||
```
|
||||
|
||||
Where `left` can be either `left`, `right`, `top` or `bottom`.
|
||||
### Nix
|
||||
- nixpkgs: [search.nixos.org](https://search.nixos.org/packages?channel=unstable&show=lan-mouse&from=0&size=50&sort=relevance&type=packages&query=lan-mouse)
|
||||
- flake: [README.md](./nix/README.md)
|
||||
|
||||
|
||||
## Build and Run
|
||||
### Building from Source
|
||||
|
||||
Build in release mode:
|
||||
```sh
|
||||
cargo build --release
|
||||
@@ -62,115 +91,225 @@ Run directly:
|
||||
cargo run --release
|
||||
```
|
||||
|
||||
Install the files:
|
||||
```sh
|
||||
# install lan-mouse
|
||||
sudo cp target/release/lan-mouse /usr/local/bin/
|
||||
|
||||
# install app icon
|
||||
sudo mkdir -p /usr/local/share/icons/hicolor/scalable/apps
|
||||
sudo cp resources/de.feschber.LanMouse.svg /usr/local/share/icons/hicolor/scalable/apps
|
||||
|
||||
# update icon cache
|
||||
gtk-update-icon-cache /usr/local/share/icons/hicolor/
|
||||
|
||||
# install desktop entry
|
||||
sudo mkdir -p /usr/local/share/applications
|
||||
sudo cp de.feschber.LanMouse.desktop /usr/local/share/applications
|
||||
|
||||
# when using firewalld: install firewall rule
|
||||
sudo cp firewall/lan-mouse.xml /etc/firewalld/services
|
||||
# -> enable the service in firewalld settings
|
||||
```
|
||||
|
||||
### Conditional Compilation
|
||||
|
||||
Currently only x11, wayland 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
|
||||
```
|
||||
|
||||
## OS Support
|
||||
|
||||
The following table shows support for Event receiving and event Emitting
|
||||
on different operating systems:
|
||||
|
||||
| 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) |
|
||||
|
||||
## Wayland compositor support
|
||||
### Input Emulation (for receiving events)
|
||||
On wayland input-emulation is in an early/unstable state as of writing this.
|
||||
|
||||
Different compositors have different ways of enabling input emulation:
|
||||
|
||||
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
|
||||
|
||||
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).
|
||||
|
||||
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.
|
||||
|
||||
| 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: |
|
||||
|
||||
### 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).
|
||||
|
||||
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.
|
||||
|
||||
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,
|
||||
event producing on windows is WIP.
|
||||
For a detailed list of available features, checkout the [Cargo.toml](./Cargo.toml)
|
||||
|
||||
|
||||
## 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
|
||||
- [x] 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
|
||||
- [x] Liveness tracking (automatically ungrab mouse when client unreachable)
|
||||
## Installing Dependencies
|
||||
|
||||
#### Macos
|
||||
```sh
|
||||
brew install libadwaita
|
||||
```
|
||||
|
||||
#### Ubuntu and derivatives
|
||||
```sh
|
||||
sudo apt install libadwaita-1-dev libgtk-4-dev libx11-dev libxtst-dev
|
||||
```
|
||||
|
||||
#### Arch and derivatives
|
||||
```sh
|
||||
sudo pacman -S libadwaita gtk libx11 libxtst
|
||||
```
|
||||
|
||||
#### Fedora and derivatives
|
||||
```sh
|
||||
sudo dnf install libadwaita-devel libXtst-devel libX11-devel
|
||||
```
|
||||
|
||||
#### Windows
|
||||
> [!NOTE]
|
||||
> This is only necessary when building lan-mouse from source. The windows release comes with precompiled gtk dlls.
|
||||
|
||||
Follow the instructions at [gtk-rs.org](https://gtk-rs.org/gtk4-rs/stable/latest/book/installation_windows.html)
|
||||
|
||||
*TLDR:*
|
||||
|
||||
Build gtk from source
|
||||
|
||||
- The following commands should be run in an admin power shell instance:
|
||||
```sh
|
||||
# install chocolatey
|
||||
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
|
||||
|
||||
# install python 3.11 (Version is important, as 3.12 does not work currently) -> Has been fixed recently
|
||||
choco install python --version=3.11.0
|
||||
|
||||
# install git
|
||||
choco install git
|
||||
|
||||
# install msys2
|
||||
choco install msys2
|
||||
|
||||
# install Visual Studio 2022
|
||||
choco install visualstudio2022-workload-vctools
|
||||
```
|
||||
|
||||
- The following commands should be run in a regular power shell instance:
|
||||
|
||||
```sh
|
||||
# install gvsbuild with python
|
||||
python -m pip install --user pipx
|
||||
python -m pipx ensurepath
|
||||
```
|
||||
|
||||
- Relaunch your powershell instance so the changes in the environment are reflected.
|
||||
```sh
|
||||
pipx install gvsbuild
|
||||
|
||||
# build gtk + libadwaita
|
||||
gvsbuild build gtk4 libadwaita librsvg
|
||||
```
|
||||
|
||||
Make sure to add the directory `C:\gtk-build\gtk\x64\release\bin`
|
||||
[to the `PATH` environment variable]((https://learn.microsoft.com/en-us/previous-versions/office/developer/sharepoint-2010/ee537574(v=office.14))). Otherwise the project will fail to build.
|
||||
|
||||
To avoid building GTK from source, it is possible to disable
|
||||
the gtk frontend (see conditional compilation below).
|
||||
|
||||
## Usage
|
||||
### Gtk Frontend
|
||||
By default the gtk frontend will open when running `lan-mouse`.
|
||||
|
||||
To add a new connection, simply click the `Add` button on *both* devices,
|
||||
enter the corresponding hostname and activate it.
|
||||
|
||||
If the mouse can not be moved onto a device, make sure you have port `4242` (or the one selected)
|
||||
opened up in your firewall.
|
||||
|
||||
### Command Line Interface
|
||||
The cli interface can be enabled using `--frontend cli` as commandline arguments.
|
||||
Type `help` to list the available commands.
|
||||
|
||||
E.g.:
|
||||
```sh
|
||||
$ cargo run --release -- --frontend cli
|
||||
(...)
|
||||
> connect <host> left|right|top|bottom
|
||||
(...)
|
||||
> list
|
||||
(...)
|
||||
> activate 0
|
||||
```
|
||||
|
||||
### 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:
|
||||
|
||||
```sh
|
||||
$ cargo run --release -- --daemon
|
||||
```
|
||||
|
||||
In order to start lan-mouse with a graphical session automatically,
|
||||
the [systemd-service](service/lan-mouse.service) can be used:
|
||||
|
||||
Copy the file to `~/.config/systemd/user/` and enable the service:
|
||||
|
||||
```sh
|
||||
cp service/lan-mouse.service ~/.config/systemd/user
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now lan-mouse.service
|
||||
```
|
||||
|
||||
## Configuration
|
||||
To automatically load clients on startup, the file `$XDG_CONFIG_HOME/lan-mouse/config.toml` is parsed.
|
||||
`$XDG_CONFIG_HOME` defaults to `~/.config/`.
|
||||
|
||||
To create this file you can copy the following example config:
|
||||
|
||||
### Example config
|
||||
> [!TIP]
|
||||
> key symbols in the release bind are named according
|
||||
> to their names in [src/scancode.rs#L172](src/scancode.rs#L172).
|
||||
> This is bound to change
|
||||
|
||||
```toml
|
||||
# example configuration
|
||||
|
||||
# configure release bind
|
||||
release_bind = [ "KeyA", "KeyS", "KeyD", "KeyF" ]
|
||||
|
||||
# optional port (defaults to 4242)
|
||||
port = 4242
|
||||
# # optional frontend -> defaults to gtk if available
|
||||
# # possible values are "cli" and "gtk"
|
||||
# frontend = "gtk"
|
||||
|
||||
# define a client on the right side with host name "iridium"
|
||||
[right]
|
||||
# hostname
|
||||
hostname = "iridium"
|
||||
# activate this client immediately when lan-mouse is started
|
||||
activate_on_startup = true
|
||||
# optional list of (known) ip addresses
|
||||
ips = ["192.168.178.156"]
|
||||
|
||||
# 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.
|
||||
hostname = "thorium"
|
||||
# ips for ethernet and wifi
|
||||
ips = ["192.168.178.189", "192.168.178.172"]
|
||||
# optional port
|
||||
port = 4242
|
||||
```
|
||||
|
||||
Where `left` can be either `left`, `right`, `top` or `bottom`.
|
||||
|
||||
## 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
|
||||
- [x] Liveness tracking: Automatically release keys, when server offline
|
||||
- [x] MacOS KeyCode Translation
|
||||
- [x] Libei Input Capture
|
||||
- [ ] X11 Input Capture
|
||||
- [ ] Windows Input Capture
|
||||
- [ ] MacOS Input Capture
|
||||
- [ ] Latency measurement and visualization
|
||||
- [ ] Bandwidth usage measurement and visualization
|
||||
- [ ] Clipboard support
|
||||
- [x] 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.
|
||||
@@ -219,3 +358,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)
|
||||
|
||||
|
||||
1
build.rs
1
build.rs
@@ -1,6 +1,5 @@
|
||||
fn main() {
|
||||
// composite_templates
|
||||
#[cfg(unix)]
|
||||
glib_build_tools::compile_resources(
|
||||
&["resources"],
|
||||
"resources/resources.gresource.xml",
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# example configuration
|
||||
|
||||
# release bind
|
||||
release_bind = [ "KeyA", "KeyS", "KeyD", "KeyF" ]
|
||||
|
||||
# optional port (defaults to 4242)
|
||||
port = 4242
|
||||
# optional frontend -> defaults to gtk if available
|
||||
@@ -8,7 +11,7 @@ port = 4242
|
||||
# define a client on the right side with host name "iridium"
|
||||
[right]
|
||||
# hostname
|
||||
host_name = "iridium"
|
||||
hostname = "iridium"
|
||||
# optional list of (known) ip addresses
|
||||
ips = ["192.168.178.156"]
|
||||
|
||||
@@ -16,7 +19,7 @@ ips = ["192.168.178.156"]
|
||||
[left]
|
||||
# The hostname is optional: When no hostname is specified,
|
||||
# at least one ip address needs to be specified.
|
||||
host_name = "thorium"
|
||||
hostname = "thorium"
|
||||
# ips for ethernet and wifi
|
||||
ips = ["192.168.178.189", "192.168.178.172"]
|
||||
# optional port
|
||||
|
||||
12
de.feschber.LanMouse.desktop
Normal file
12
de.feschber.LanMouse.desktop
Normal file
@@ -0,0 +1,12 @@
|
||||
[Desktop Entry]
|
||||
Categories=Utility;
|
||||
Comment[en_US]=Mouse & Keyboard sharing via LAN
|
||||
Comment=Mouse & Keyboard sharing via LAN
|
||||
Comment[de_DE]=Maus- und Tastaturfreigabe über LAN
|
||||
Exec=lan-mouse
|
||||
Icon=de.feschber.LanMouse
|
||||
Name[en_US]=Lan Mouse
|
||||
Name=Lan Mouse
|
||||
StartupNotify=true
|
||||
Terminal=false
|
||||
Type=Application
|
||||
8
firewall/lan-mouse.xml
Normal file
8
firewall/lan-mouse.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- for packaging: /usr/lib/firewalld/services/lan-mouse.xml -->
|
||||
<!-- configure manually: /etc/firewalld/services/lan-mouse.xml -->
|
||||
<service>
|
||||
<short>LAN Mouse</short>
|
||||
<description>mouse and keyboard sharing via LAN</description>
|
||||
<port port="4242" protocol="udp"/>
|
||||
</service>
|
||||
82
flake.lock
generated
Normal file
82
flake.lock
generated
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1705309234,
|
||||
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1706913249,
|
||||
"narHash": "sha256-x3M7iV++CsvRXI1fpyFPduGELUckZEhSv0XWnUopAG8=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e92b6015881907e698782c77641aa49298330223",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1707099356,
|
||||
"narHash": "sha256-ph483MDKLi9I/gndYOieVP41es633DOOmPjEI50x5KU=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "61dfa5a8129f7edbe9150253c68f673f87b16fb1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
58
flake.nix
Normal file
58
flake.nix
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
description = "Nix Flake for lan-mouse";
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
rust-overlay,
|
||||
...
|
||||
}: let
|
||||
inherit (nixpkgs) lib;
|
||||
genSystems = lib.genAttrs [
|
||||
"x86_64-linux"
|
||||
];
|
||||
pkgsFor = system:
|
||||
import nixpkgs {
|
||||
inherit system;
|
||||
|
||||
overlays = [
|
||||
rust-overlay.overlays.default
|
||||
];
|
||||
};
|
||||
mkRustToolchain = pkgs:
|
||||
pkgs.rust-bin.stable.latest.default.override {
|
||||
extensions = ["rust-src"];
|
||||
};
|
||||
pkgs = genSystems (system: import nixpkgs {inherit system;});
|
||||
in {
|
||||
packages = genSystems (system: rec {
|
||||
default = pkgs.${system}.callPackage ./nix {};
|
||||
lan-mouse = default;
|
||||
});
|
||||
homeManagerModules.default = import ./nix/hm-module.nix self;
|
||||
devShells = genSystems (system: let
|
||||
pkgs = pkgsFor system;
|
||||
rust = mkRustToolchain pkgs;
|
||||
in {
|
||||
default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
rust
|
||||
rust-analyzer-unwrapped
|
||||
pkg-config
|
||||
xorg.libX11
|
||||
gtk4
|
||||
libadwaita
|
||||
xorg.libXtst
|
||||
];
|
||||
|
||||
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
40
nix/README.md
Normal file
40
nix/README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Nix Flake Usage
|
||||
|
||||
## run
|
||||
|
||||
```bash
|
||||
nix run github:feschber/lan-mouse
|
||||
|
||||
# with params
|
||||
nix run github:feschber/lan-mouse -- --help
|
||||
|
||||
```
|
||||
|
||||
## home-manager module
|
||||
|
||||
add input
|
||||
|
||||
```nix
|
||||
inputs = {
|
||||
lan-mouse.url = "github:feschber/lan-mouse";
|
||||
}
|
||||
```
|
||||
|
||||
enable lan-mouse
|
||||
|
||||
``` nix
|
||||
{
|
||||
inputs,
|
||||
...
|
||||
}: {
|
||||
# add the home manager module
|
||||
imports = [inputs.lan-mouse.homeManagerModules.default];
|
||||
|
||||
programs.lan-mouse = {
|
||||
enable = true;
|
||||
# systemd = false;
|
||||
# package = inputs.lan-mouse.packages.${pkgs.stdenv.hostPlatform.system}.default
|
||||
};
|
||||
}
|
||||
|
||||
```
|
||||
46
nix/default.nix
Normal file
46
nix/default.nix
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
rustPlatform,
|
||||
lib,
|
||||
pkgs,
|
||||
}:
|
||||
rustPlatform.buildRustPackage {
|
||||
pname = "lan-mouse";
|
||||
version = "0.7.0";
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
pkg-config
|
||||
cmake
|
||||
buildPackages.gtk4
|
||||
];
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
xorg.libX11
|
||||
gtk4
|
||||
libadwaita
|
||||
xorg.libXtst
|
||||
];
|
||||
|
||||
src = builtins.path {
|
||||
name = "lan-mouse";
|
||||
path = lib.cleanSource ../.;
|
||||
};
|
||||
|
||||
cargoLock.lockFile = ../Cargo.lock;
|
||||
|
||||
cargoLock.outputHashes = {
|
||||
"reis-0.1.0" = "sha256-QhsRIkzW3wgOlcHpkx3axjS8Vfed00Uf36av9ossPwQ=";
|
||||
};
|
||||
|
||||
# Set Environment Variables
|
||||
RUST_BACKTRACE = "full";
|
||||
|
||||
meta = with lib; {
|
||||
description = "Lan Mouse is a mouse and keyboard sharing software";
|
||||
longDescription = ''
|
||||
Lan Mouse is a mouse and keyboard sharing software similar to universal-control on Apple devices. It allows for using multiple pcs with a single set of mouse and keyboard. This is also known as a Software KVM switch.
|
||||
The primary target is Wayland on Linux but Windows and MacOS and Linux on Xorg have partial support as well (see below for more details).
|
||||
'';
|
||||
mainProgram = "lan-mouse";
|
||||
platforms = platforms.all;
|
||||
};
|
||||
}
|
||||
50
nix/hm-module.nix
Normal file
50
nix/hm-module.nix
Normal file
@@ -0,0 +1,50 @@
|
||||
self: {
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
with lib; let
|
||||
cfg = config.programs.lan-mouse;
|
||||
defaultPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
||||
in {
|
||||
options.programs.lan-mouse = with types; {
|
||||
enable = mkEnableOption "Whether or not to enable lan-mouse.";
|
||||
package = mkOption {
|
||||
type = with types; nullOr package;
|
||||
default = defaultPackage;
|
||||
defaultText = literalExpression "inputs.lan-mouse.packages.${pkgs.stdenv.hostPlatform.system}.default";
|
||||
description = ''
|
||||
The lan-mouse package to use.
|
||||
|
||||
By default, this option will use the `packages.default` as exposed by this flake.
|
||||
'';
|
||||
};
|
||||
systemd = mkOption {
|
||||
type = types.bool;
|
||||
default = pkgs.stdenv.isLinux;
|
||||
description = "Whether to enable to systemd service for lan-mouse.";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
systemd.user.services.lan-mouse = lib.mkIf cfg.systemd {
|
||||
Unit = {
|
||||
Description = "Systemd service for Lan Mouse";
|
||||
Requires = ["graphical-session.target"];
|
||||
};
|
||||
Service = {
|
||||
Type = "simple";
|
||||
ExecStart = "${cfg.package}/bin/lan-mouse --daemon";
|
||||
};
|
||||
Install.WantedBy = [
|
||||
(lib.mkIf config.wayland.windowManager.hyprland.systemd.enable "hyprland-session.target")
|
||||
(lib.mkIf config.wayland.windowManager.sway.systemd.enable "sway-session.target")
|
||||
];
|
||||
};
|
||||
|
||||
home.packages = [
|
||||
cfg.package
|
||||
];
|
||||
};
|
||||
}
|
||||
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
@@ -7,6 +7,6 @@
|
||||
<file compressed="true">style-dark.css</file>
|
||||
</gresource>
|
||||
<gresource prefix="/de/feschber/LanMouse/icons">
|
||||
<file compressed="true" preprocess="xml-stripblanks">mouse-icon.svg</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">de.feschber.LanMouse.svg</file>
|
||||
</gresource>
|
||||
</gresources>
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
#delete-button {
|
||||
color: @red_1;
|
||||
}
|
||||
|
||||
#port-edit-cancel {
|
||||
color: @red_1;
|
||||
}
|
||||
|
||||
#port-edit-apply {
|
||||
color: @green_1;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
#delete-button {
|
||||
color: @red_3;
|
||||
}
|
||||
|
||||
#port-edit-cancel {
|
||||
color: @red_3;
|
||||
}
|
||||
|
||||
#port-edit-apply {
|
||||
color: @green_3;
|
||||
}
|
||||
|
||||
@@ -8,114 +8,139 @@
|
||||
<attribute name="action">window.close</attribute>
|
||||
</item>
|
||||
</menu>
|
||||
<template class="LanMouseWindow" parent="GtkApplicationWindow">
|
||||
<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>
|
||||
<child type="titlebar">
|
||||
<object class="GtkHeaderBar">
|
||||
<child type ="end">
|
||||
<object class="GtkMenuButton">
|
||||
<property name="icon-name">open-menu-symbolic</property>
|
||||
<property name="menu-model">main-menu</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>
|
||||
<style>
|
||||
<class name="flat"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<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="AdwToastOverlay" id="toast_overlay">
|
||||
<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>
|
||||
<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">de.feschber.LanMouse</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="GtkEntry">
|
||||
<!-- <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 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>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup">
|
||||
<property name="title" translatable="yes">Connections</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 below</property>
|
||||
<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>
|
||||
<style>
|
||||
<class name="boxed-list" />
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup">
|
||||
<child>
|
||||
<object class="GtkButton" id="add_client_button">
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="tooltip-text">connect a new computer</property>
|
||||
<property name="child">
|
||||
<object class="AdwButtonContent">
|
||||
<property name="icon-name">list-add-symbolic</property>
|
||||
<property name="label" translatable="yes">Add</property>
|
||||
<property name="use-underline">True</property>
|
||||
</object>
|
||||
</property>
|
||||
<!-- <signal name="clicked" handler="handle_add_client" swapped="true"/> -->
|
||||
<style>
|
||||
<class name="pill"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</property>
|
||||
</template>
|
||||
</interface>
|
||||
|
||||
13
service/lan-mouse.service
Normal file
13
service/lan-mouse.service
Normal file
@@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=Lan Mouse
|
||||
# lan mouse needs an active graphical session
|
||||
After=graphical-session.target
|
||||
# make sure the service terminates with the graphical session
|
||||
BindsTo=graphical-session.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/lan-mouse --daemon
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=graphical-session.target
|
||||
1
shell.nix
Normal file
1
shell.nix
Normal file
@@ -0,0 +1 @@
|
||||
(builtins.getFlake ("git+file://" + toString ./.)).devShells.${builtins.currentSystem}.default
|
||||
@@ -1,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,387 @@
|
||||
use crate::consumer::EventConsumer;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
os::{fd::OwnedFd, unix::net::UnixStream},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
pub struct LibeiConsumer {}
|
||||
use anyhow::{anyhow, Result};
|
||||
use ashpd::{
|
||||
desktop::{
|
||||
remote_desktop::{DeviceType, RemoteDesktop},
|
||||
ResponseError,
|
||||
},
|
||||
WindowIdentifier,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
|
||||
use reis::{
|
||||
ei::{self, button::ButtonState, handshake::ContextType, keyboard::KeyState},
|
||||
tokio::EiEventStream,
|
||||
PendingRequestResult,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
client::{ClientEvent, ClientHandle},
|
||||
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<OwnedFd, ashpd::Error> {
|
||||
let proxy = RemoteDesktop::new().await?;
|
||||
|
||||
// retry when user presses the cancel button
|
||||
let (session, _) = loop {
|
||||
log::debug!("creating session ...");
|
||||
let session = proxy.create_session().await?;
|
||||
|
||||
log::debug!("selecting devices ...");
|
||||
proxy
|
||||
.select_devices(&session, DeviceType::Keyboard | DeviceType::Pointer)
|
||||
.await?;
|
||||
|
||||
log::info!("requesting permission for input emulation");
|
||||
match proxy
|
||||
.start(&session, &WindowIdentifier::default())
|
||||
.await?
|
||||
.response()
|
||||
{
|
||||
Ok(d) => break (session, d),
|
||||
Err(ashpd::Error::Response(ResponseError::Cancelled)) => {
|
||||
log::warn!("request cancelled!");
|
||||
continue;
|
||||
}
|
||||
e => e?,
|
||||
};
|
||||
};
|
||||
|
||||
proxy.connect_to_eis(&session).await
|
||||
}
|
||||
|
||||
impl 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 stream = UnixStream::from(eifd);
|
||||
// let stream = UnixStream::connect("/run/user/1000/eis-0")?;
|
||||
stream.set_nonblocking(true)?;
|
||||
let context = ei::Context::new(stream)?;
|
||||
context.flush()?;
|
||||
let events = EiEventStream::new(context.clone())?;
|
||||
Ok(Self {
|
||||
handshake: false,
|
||||
context,
|
||||
events,
|
||||
pointer: None,
|
||||
button: None,
|
||||
scroll: None,
|
||||
keyboard: None,
|
||||
has_pointer: false,
|
||||
has_button: false,
|
||||
has_scroll: false,
|
||||
has_keyboard: false,
|
||||
capabilities: HashMap::new(),
|
||||
capability_mask: 0,
|
||||
sequence: 0,
|
||||
serial: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl 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) {}
|
||||
}
|
||||
|
||||
279
src/backend/consumer/macos.rs
Normal file
279
src/backend/consumer/macos.rs
Normal file
@@ -0,0 +1,279 @@
|
||||
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, CGKeyCode, CGMouseButton, EventField, ScrollEventUnit,
|
||||
};
|
||||
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
|
||||
use keycode::{KeyMap, KeyMapping};
|
||||
use std::ops::{Index, IndexMut};
|
||||
use std::time::Duration;
|
||||
use tokio::task::AbortHandle;
|
||||
|
||||
const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500);
|
||||
const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32);
|
||||
|
||||
pub struct MacOSConsumer {
|
||||
pub event_source: CGEventSource,
|
||||
repeat_task: Option<AbortHandle>,
|
||||
button_state: ButtonState,
|
||||
}
|
||||
|
||||
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,
|
||||
repeat_task: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_mouse_location(&self) -> Option<CGPoint> {
|
||||
let event: CGEvent = CGEvent::new(self.event_source.clone()).ok()?;
|
||||
Some(event.location())
|
||||
}
|
||||
|
||||
async fn spawn_repeat_task(&mut self, key: u16) {
|
||||
// there can only be one repeating key and it's
|
||||
// always the last to be pressed
|
||||
self.kill_repeat_task();
|
||||
let event_source = self.event_source.clone();
|
||||
let repeat_task = tokio::task::spawn_local(async move {
|
||||
tokio::time::sleep(DEFAULT_REPEAT_DELAY).await;
|
||||
loop {
|
||||
key_event(event_source.clone(), key, 1);
|
||||
tokio::time::sleep(DEFAULT_REPEAT_INTERVAL).await;
|
||||
}
|
||||
});
|
||||
self.repeat_task = Some(repeat_task.abort_handle());
|
||||
}
|
||||
fn kill_repeat_task(&mut self) {
|
||||
if let Some(task) = self.repeat_task.take() {
|
||||
task.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn key_event(event_source: CGEventSource, key: u16, state: u8) {
|
||||
let event = match CGEvent::new_keyboard_event(event_source, key, state != 0) {
|
||||
Ok(e) => e,
|
||||
Err(_) => {
|
||||
log::warn!("unable to create key event");
|
||||
return;
|
||||
}
|
||||
};
|
||||
event.post(CGEventTapLocation::HID);
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
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 {
|
||||
time: _,
|
||||
key,
|
||||
state,
|
||||
} => {
|
||||
let code = match KeyMap::from_key_mapping(KeyMapping::Evdev(key as u16)) {
|
||||
Ok(k) => k.mac as CGKeyCode,
|
||||
Err(_) => {
|
||||
log::warn!("unable to map key event");
|
||||
return;
|
||||
}
|
||||
};
|
||||
match state {
|
||||
// pressed
|
||||
1 => self.spawn_repeat_task(code).await,
|
||||
_ => self.kill_repeat_task(),
|
||||
}
|
||||
key_event(self.event_source.clone(), code, state)
|
||||
}
|
||||
KeyboardEvent::Modifiers { .. } => {}
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
async fn notify(&mut self, _client_event: ClientEvent) {}
|
||||
|
||||
async fn destroy(&mut self) {}
|
||||
}
|
||||
@@ -1,15 +1,20 @@
|
||||
use crate::{event::{KeyboardEvent, PointerEvent}, consumer::EventConsumer};
|
||||
use crate::{
|
||||
consumer::EventConsumer,
|
||||
event::{KeyboardEvent, PointerEvent},
|
||||
scancode,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use std::time::Duration;
|
||||
use tokio::task::AbortHandle;
|
||||
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 +23,22 @@ use crate::{
|
||||
event::Event,
|
||||
};
|
||||
|
||||
const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500);
|
||||
const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32);
|
||||
|
||||
pub struct WindowsConsumer {}
|
||||
|
||||
impl WindowsConsumer {
|
||||
pub fn new() -> Self { Self { } }
|
||||
pub struct WindowsConsumer {
|
||||
repeat_task: Option<AbortHandle>,
|
||||
}
|
||||
|
||||
impl WindowsConsumer {
|
||||
pub fn new() -> Result<Self> {
|
||||
Ok(Self { repeat_task: None })
|
||||
}
|
||||
}
|
||||
|
||||
#[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 +48,64 @@ 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,
|
||||
} => {
|
||||
match state {
|
||||
// pressed
|
||||
0 => self.kill_repeat_task(),
|
||||
1 => self.spawn_repeat_task(key).await,
|
||||
_ => {}
|
||||
}
|
||||
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) {}
|
||||
}
|
||||
|
||||
impl WindowsConsumer {
|
||||
async fn spawn_repeat_task(&mut self, key: u32) {
|
||||
// there can only be one repeating key and it's
|
||||
// always the last to be pressed
|
||||
self.kill_repeat_task();
|
||||
let repeat_task = tokio::task::spawn_local(async move {
|
||||
tokio::time::sleep(DEFAULT_REPEAT_DELAY).await;
|
||||
loop {
|
||||
key_event(key, 1);
|
||||
tokio::time::sleep(DEFAULT_REPEAT_INTERVAL).await;
|
||||
}
|
||||
});
|
||||
self.repeat_task = Some(repeat_task.abort_handle());
|
||||
}
|
||||
fn kill_repeat_task(&mut self) {
|
||||
if let Some(task) = self.repeat_task.take() {
|
||||
task.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_mouse_input(mi: MOUSEINPUT) {
|
||||
@@ -60,8 +115,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 +141,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 +166,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 +180,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 +209,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,16 +151,17 @@ 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 {
|
||||
@@ -171,96 +169,39 @@ impl VirtualInput {
|
||||
time,
|
||||
relative_x,
|
||||
relative_y,
|
||||
} => match self {
|
||||
VirtualInput::Wlroots {
|
||||
pointer,
|
||||
keyboard: _,
|
||||
} => {
|
||||
pointer.motion(time, relative_x, relative_y);
|
||||
}
|
||||
VirtualInput::Kde { fake_input } => {
|
||||
fake_input.pointer_motion(relative_y, relative_y);
|
||||
}
|
||||
},
|
||||
} => self.pointer.motion(time, relative_x, 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);
|
||||
}
|
||||
}
|
||||
self.pointer.button(time, button, state);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
self.pointer.axis(time, axis, value);
|
||||
self.pointer.frame();
|
||||
}
|
||||
PointerEvent::Frame {} => match self {
|
||||
VirtualInput::Wlroots {
|
||||
pointer,
|
||||
keyboard: _,
|
||||
} => {
|
||||
pointer.frame();
|
||||
}
|
||||
VirtualInput::Kde { fake_input: _ } => {}
|
||||
},
|
||||
PointerEvent::Frame {} => self.pointer.frame(),
|
||||
}
|
||||
match self {
|
||||
VirtualInput::Wlroots { pointer, .. } => {
|
||||
// insert a frame event after each mouse event
|
||||
pointer.frame();
|
||||
}
|
||||
_ => {},
|
||||
}
|
||||
},
|
||||
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(())
|
||||
}
|
||||
@@ -270,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(
|
||||
@@ -293,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,154 @@
|
||||
use crate::consumer::EventConsumer;
|
||||
use anyhow::Result;
|
||||
use ashpd::{
|
||||
desktop::{
|
||||
remote_desktop::{Axis, DeviceType, KeyState, RemoteDesktop},
|
||||
ResponseError, 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?;
|
||||
|
||||
// retry when user presses the cancel button
|
||||
let (session, _) = loop {
|
||||
log::debug!("creating session ...");
|
||||
let session = proxy.create_session().await?;
|
||||
|
||||
log::debug!("selecting devices ...");
|
||||
proxy
|
||||
.select_devices(&session, DeviceType::Keyboard | DeviceType::Pointer)
|
||||
.await?;
|
||||
|
||||
log::info!("requesting permission for input emulation");
|
||||
match proxy
|
||||
.start(&session, &WindowIdentifier::default())
|
||||
.await?
|
||||
.response()
|
||||
{
|
||||
Ok(d) => break (session, d),
|
||||
Err(ashpd::Error::Response(ResponseError::Cancelled)) => {
|
||||
log::warn!("request cancelled!");
|
||||
continue;
|
||||
}
|
||||
e => e?,
|
||||
};
|
||||
};
|
||||
|
||||
log::debug!("started session");
|
||||
|
||||
Ok(Self { proxy, session })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'a> 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;
|
||||
|
||||
42
src/backend/producer/dummy.rs
Normal file
42
src/backend/producer/dummy.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
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, _event: ClientEvent) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn release(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
555
src/backend/producer/libei.rs
Normal file
555
src/backend/producer/libei.rs
Normal file
@@ -0,0 +1,555 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use ashpd::{
|
||||
desktop::{
|
||||
input_capture::{Activated, Barrier, BarrierID, Capabilities, InputCapture, Region, Zones},
|
||||
ResponseError, Session,
|
||||
},
|
||||
enumflags2::BitFlags,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use reis::{
|
||||
ei::{self, keyboard::KeyState},
|
||||
eis::button::ButtonState,
|
||||
event::{DeviceCapability, EiEvent},
|
||||
tokio::{EiConvertEventStream, EiEventStream},
|
||||
};
|
||||
use std::{
|
||||
cell::Cell,
|
||||
collections::HashMap,
|
||||
io,
|
||||
os::unix::net::UnixStream,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
task::{ready, Context, Poll},
|
||||
};
|
||||
use tokio::{
|
||||
sync::mpsc::{Receiver, Sender},
|
||||
task::JoinHandle,
|
||||
};
|
||||
|
||||
use futures_core::Stream;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::{
|
||||
client::{ClientEvent, ClientHandle, Position},
|
||||
event::{Event, KeyboardEvent, PointerEvent},
|
||||
producer::EventProducer,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ProducerEvent {
|
||||
Release,
|
||||
ClientEvent(ClientEvent),
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct LibeiProducer<'a> {
|
||||
input_capture: Pin<Box<InputCapture<'a>>>,
|
||||
libei_task: JoinHandle<Result<()>>,
|
||||
event_rx: tokio::sync::mpsc::Receiver<(u32, Event)>,
|
||||
notify_tx: tokio::sync::mpsc::Sender<ProducerEvent>,
|
||||
}
|
||||
|
||||
static INTERFACES: Lazy<HashMap<&'static str, u32>> = Lazy::new(|| {
|
||||
let mut m = HashMap::new();
|
||||
m.insert("ei_connection", 1);
|
||||
m.insert("ei_callback", 1);
|
||||
m.insert("ei_pingpong", 1);
|
||||
m.insert("ei_seat", 1);
|
||||
m.insert("ei_device", 2);
|
||||
m.insert("ei_pointer", 1);
|
||||
m.insert("ei_pointer_absolute", 1);
|
||||
m.insert("ei_scroll", 1);
|
||||
m.insert("ei_button", 1);
|
||||
m.insert("ei_keyboard", 1);
|
||||
m.insert("ei_touchscreen", 1);
|
||||
m
|
||||
});
|
||||
|
||||
fn pos_to_barrier(r: &Region, pos: Position) -> (i32, i32, i32, i32) {
|
||||
let (x, y) = (r.x_offset(), r.y_offset());
|
||||
let (width, height) = (r.width() as i32, r.height() as i32);
|
||||
match pos {
|
||||
Position::Left => (x, y, x, y + height - 1), // start pos, end pos, inclusive
|
||||
Position::Right => (x + width, y, x + width, y + height - 1),
|
||||
Position::Top => (x, y, x + width - 1, y),
|
||||
Position::Bottom => (x, y + height, x + width - 1, y + height),
|
||||
}
|
||||
}
|
||||
|
||||
fn select_barriers(
|
||||
zones: &Zones,
|
||||
clients: &Vec<(ClientHandle, Position)>,
|
||||
next_barrier_id: &mut u32,
|
||||
) -> (Vec<Barrier>, HashMap<BarrierID, ClientHandle>) {
|
||||
let mut client_for_barrier = HashMap::new();
|
||||
let mut barriers: Vec<Barrier> = vec![];
|
||||
|
||||
for (handle, pos) in clients {
|
||||
let mut client_barriers = zones
|
||||
.regions()
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let id = *next_barrier_id;
|
||||
*next_barrier_id = id + 1;
|
||||
let position = pos_to_barrier(r, *pos);
|
||||
client_for_barrier.insert(id, *handle);
|
||||
Barrier::new(id, position)
|
||||
})
|
||||
.collect();
|
||||
barriers.append(&mut client_barriers);
|
||||
}
|
||||
(barriers, client_for_barrier)
|
||||
}
|
||||
|
||||
async fn update_barriers(
|
||||
input_capture: &InputCapture<'_>,
|
||||
session: &Session<'_>,
|
||||
active_clients: &Vec<(ClientHandle, Position)>,
|
||||
next_barrier_id: &mut u32,
|
||||
) -> Result<HashMap<BarrierID, ClientHandle>> {
|
||||
let zones = input_capture.zones(session).await?.response()?;
|
||||
log::debug!("zones: {zones:?}");
|
||||
|
||||
let (barriers, id_map) = select_barriers(&zones, active_clients, next_barrier_id);
|
||||
log::debug!("barriers: {barriers:?}");
|
||||
log::debug!("client for barrier id: {id_map:?}");
|
||||
|
||||
let response = input_capture
|
||||
.set_pointer_barriers(session, &barriers, zones.zone_set())
|
||||
.await?;
|
||||
let response = response.response()?;
|
||||
log::info!("{response:?}");
|
||||
Ok(id_map)
|
||||
}
|
||||
|
||||
impl<'a> Drop for LibeiProducer<'a> {
|
||||
fn drop(&mut self) {
|
||||
self.libei_task.abort();
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_session<'a>(
|
||||
input_capture: &'a InputCapture<'a>,
|
||||
) -> Result<(Session<'a>, BitFlags<Capabilities>)> {
|
||||
log::info!("creating input capture session");
|
||||
let (session, capabilities) = loop {
|
||||
match input_capture
|
||||
.create_session(
|
||||
&ashpd::WindowIdentifier::default(),
|
||||
Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(s) => break s,
|
||||
Err(ashpd::Error::Response(ResponseError::Cancelled)) => continue,
|
||||
o => o?,
|
||||
};
|
||||
};
|
||||
log::debug!("capabilities: {capabilities:?}");
|
||||
Ok((session, capabilities))
|
||||
}
|
||||
|
||||
async fn connect_to_eis(
|
||||
input_capture: &InputCapture<'_>,
|
||||
session: &Session<'_>,
|
||||
) -> Result<(ei::Context, EiConvertEventStream)> {
|
||||
log::info!("connect_to_eis");
|
||||
let fd = input_capture.connect_to_eis(session).await?;
|
||||
|
||||
// create unix stream from fd
|
||||
let stream = UnixStream::from(fd);
|
||||
stream.set_nonblocking(true)?;
|
||||
|
||||
// create ei context
|
||||
let context = ei::Context::new(stream)?;
|
||||
let mut event_stream = EiEventStream::new(context.clone())?;
|
||||
let response = match reis::tokio::ei_handshake(
|
||||
&mut event_stream,
|
||||
"de.feschber.LanMouse",
|
||||
ei::handshake::ContextType::Receiver,
|
||||
&INTERFACES,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(res) => res,
|
||||
Err(e) => return Err(anyhow!("ei handshake failed: {e:?}")),
|
||||
};
|
||||
let event_stream = EiConvertEventStream::new(event_stream, response.serial);
|
||||
|
||||
Ok((context, event_stream))
|
||||
}
|
||||
|
||||
async fn libei_event_handler(
|
||||
mut ei_event_stream: EiConvertEventStream,
|
||||
context: ei::Context,
|
||||
event_tx: Sender<(u32, Event)>,
|
||||
current_client: Rc<Cell<Option<ClientHandle>>>,
|
||||
) -> Result<()> {
|
||||
loop {
|
||||
let ei_event = match ei_event_stream.next().await {
|
||||
Some(Ok(event)) => event,
|
||||
Some(Err(e)) => return Err(anyhow!("libei connection closed: {e:?}")),
|
||||
None => return Err(anyhow!("libei connection closed")),
|
||||
};
|
||||
log::trace!("from ei: {ei_event:?}");
|
||||
let client = current_client.get();
|
||||
handle_ei_event(ei_event, client, &context, &event_tx).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_active_client(
|
||||
notify_rx: &mut Receiver<ProducerEvent>,
|
||||
active_clients: &mut Vec<(ClientHandle, Position)>,
|
||||
) -> Result<()> {
|
||||
// wait for a client update
|
||||
while let Some(producer_event) = notify_rx.recv().await {
|
||||
if let ProducerEvent::ClientEvent(c) = producer_event {
|
||||
handle_producer_event(ProducerEvent::ClientEvent(c), active_clients)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl<'a> LibeiProducer<'a> {
|
||||
pub async fn new() -> Result<Self> {
|
||||
let input_capture = Box::pin(InputCapture::new().await?);
|
||||
let input_capture_ptr = input_capture.as_ref().get_ref() as *const InputCapture<'static>;
|
||||
let mut first_session = Some(create_session(unsafe { &*input_capture_ptr }).await?);
|
||||
|
||||
let (event_tx, event_rx) = tokio::sync::mpsc::channel(32);
|
||||
let (notify_tx, mut notify_rx) = tokio::sync::mpsc::channel(32);
|
||||
let libei_task = tokio::task::spawn_local(async move {
|
||||
/* safety: libei_task does not outlive Self */
|
||||
let input_capture = unsafe { &*input_capture_ptr };
|
||||
|
||||
let mut active_clients: Vec<(ClientHandle, Position)> = vec![];
|
||||
let mut next_barrier_id = 1u32;
|
||||
|
||||
/* there is a bug in xdg-remote-desktop-portal-gnome / mutter that
|
||||
* prevents receiving further events after a session has been disabled once.
|
||||
* Therefore the session needs to recreated when the barriers are updated */
|
||||
|
||||
loop {
|
||||
// otherwise it asks to capture input even with no active clients
|
||||
if active_clients.is_empty() {
|
||||
wait_for_active_client(&mut notify_rx, &mut active_clients).await?;
|
||||
continue;
|
||||
}
|
||||
|
||||
let current_client = Rc::new(Cell::new(None));
|
||||
|
||||
// create session
|
||||
let (session, _) = match first_session.take() {
|
||||
Some(s) => s,
|
||||
_ => create_session(input_capture).await?,
|
||||
};
|
||||
|
||||
// connect to eis server
|
||||
let (context, ei_event_stream) = connect_to_eis(input_capture, &session).await?;
|
||||
|
||||
// async event task
|
||||
let mut ei_task: JoinHandle<Result<(), anyhow::Error>> =
|
||||
tokio::task::spawn_local(libei_event_handler(
|
||||
ei_event_stream,
|
||||
context,
|
||||
event_tx.clone(),
|
||||
current_client.clone(),
|
||||
));
|
||||
|
||||
let mut activated = input_capture.receive_activated().await?;
|
||||
let mut zones_changed = input_capture.receive_zones_changed().await?;
|
||||
|
||||
// set barriers
|
||||
let client_for_barrier_id = update_barriers(
|
||||
input_capture,
|
||||
&session,
|
||||
&active_clients,
|
||||
&mut next_barrier_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
log::info!("enabling session");
|
||||
input_capture.enable(&session).await?;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
activated = activated.next() => {
|
||||
let activated = activated.ok_or(anyhow!("error receiving activation token"))?;
|
||||
log::debug!("activated: {activated:?}");
|
||||
|
||||
let client = *client_for_barrier_id
|
||||
.get(&activated.barrier_id())
|
||||
.expect("invalid barrier id");
|
||||
current_client.replace(Some(client));
|
||||
|
||||
event_tx.send((client, Event::Enter())).await?;
|
||||
|
||||
tokio::select! {
|
||||
producer_event = notify_rx.recv() => {
|
||||
let producer_event = producer_event.expect("channel closed");
|
||||
if handle_producer_event(producer_event, &mut active_clients)? {
|
||||
break; /* clients updated */
|
||||
}
|
||||
}
|
||||
zones_changed = zones_changed.next() => {
|
||||
log::debug!("zones changed: {zones_changed:?}");
|
||||
break;
|
||||
}
|
||||
res = &mut ei_task => {
|
||||
if let Err(e) = res.expect("ei task paniced") {
|
||||
log::warn!("libei task exited: {e}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
release_capture(
|
||||
input_capture,
|
||||
&session,
|
||||
activated,
|
||||
client,
|
||||
&active_clients,
|
||||
).await?;
|
||||
}
|
||||
producer_event = notify_rx.recv() => {
|
||||
let producer_event = producer_event.expect("channel closed");
|
||||
if handle_producer_event(producer_event, &mut active_clients)? {
|
||||
/* clients updated */
|
||||
break;
|
||||
}
|
||||
},
|
||||
res = &mut ei_task => {
|
||||
if let Err(e) = res.expect("ei task paniced") {
|
||||
log::warn!("libei task exited: {e}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ei_task.abort();
|
||||
input_capture.disable(&session).await?;
|
||||
}
|
||||
});
|
||||
|
||||
let producer = Self {
|
||||
input_capture,
|
||||
event_rx,
|
||||
libei_task,
|
||||
notify_tx,
|
||||
};
|
||||
|
||||
Ok(producer)
|
||||
}
|
||||
}
|
||||
|
||||
async fn release_capture(
|
||||
input_capture: &InputCapture<'_>,
|
||||
session: &Session<'_>,
|
||||
activated: Activated,
|
||||
current_client: ClientHandle,
|
||||
active_clients: &[(ClientHandle, Position)],
|
||||
) -> Result<()> {
|
||||
log::debug!("releasing input capture {}", activated.activation_id());
|
||||
let (x, y) = activated.cursor_position();
|
||||
let pos = active_clients
|
||||
.iter()
|
||||
.filter(|(c, _)| *c == current_client)
|
||||
.map(|(_, p)| p)
|
||||
.next()
|
||||
.unwrap(); // FIXME
|
||||
let (dx, dy) = match pos {
|
||||
// offset cursor position to not enter again immediately
|
||||
Position::Left => (1., 0.),
|
||||
Position::Right => (-1., 0.),
|
||||
Position::Top => (0., 1.),
|
||||
Position::Bottom => (0., -1.),
|
||||
};
|
||||
// release 1px to the right of the entered zone
|
||||
let cursor_position = (x as f64 + dx, y as f64 + dy);
|
||||
input_capture
|
||||
.release(session, activated.activation_id(), cursor_position)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_producer_event(
|
||||
producer_event: ProducerEvent,
|
||||
active_clients: &mut Vec<(ClientHandle, Position)>,
|
||||
) -> Result<bool> {
|
||||
log::debug!("handling event: {producer_event:?}");
|
||||
let updated = match producer_event {
|
||||
ProducerEvent::Release => false,
|
||||
ProducerEvent::ClientEvent(ClientEvent::Create(c, p)) => {
|
||||
active_clients.push((c, p));
|
||||
true
|
||||
}
|
||||
ProducerEvent::ClientEvent(ClientEvent::Destroy(c)) => {
|
||||
active_clients.retain(|(h, _)| *h != c);
|
||||
true
|
||||
}
|
||||
};
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
async fn handle_ei_event(
|
||||
ei_event: EiEvent,
|
||||
current_client: Option<ClientHandle>,
|
||||
context: &ei::Context,
|
||||
event_tx: &Sender<(u32, Event)>,
|
||||
) {
|
||||
match ei_event {
|
||||
EiEvent::SeatAdded(s) => {
|
||||
s.seat.bind_capabilities(&[
|
||||
DeviceCapability::Pointer,
|
||||
DeviceCapability::PointerAbsolute,
|
||||
DeviceCapability::Keyboard,
|
||||
DeviceCapability::Touch,
|
||||
DeviceCapability::Scroll,
|
||||
DeviceCapability::Button,
|
||||
]);
|
||||
context.flush().unwrap();
|
||||
}
|
||||
EiEvent::SeatRemoved(_) => {}
|
||||
EiEvent::DeviceAdded(_) => {}
|
||||
EiEvent::DeviceRemoved(_) => {}
|
||||
EiEvent::DevicePaused(_) => {}
|
||||
EiEvent::DeviceResumed(_) => {}
|
||||
EiEvent::KeyboardModifiers(mods) => {
|
||||
let modifier_event = KeyboardEvent::Modifiers {
|
||||
mods_depressed: mods.depressed,
|
||||
mods_latched: mods.latched,
|
||||
mods_locked: mods.locked,
|
||||
group: mods.group,
|
||||
};
|
||||
if let Some(current_client) = current_client {
|
||||
event_tx
|
||||
.send((current_client, Event::Keyboard(modifier_event)))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
EiEvent::Frame(_) => {}
|
||||
EiEvent::DeviceStartEmulating(_) => {
|
||||
log::debug!("START EMULATING =============>");
|
||||
}
|
||||
EiEvent::DeviceStopEmulating(_) => {
|
||||
log::debug!("==================> STOP EMULATING");
|
||||
}
|
||||
EiEvent::PointerMotion(motion) => {
|
||||
let motion_event = PointerEvent::Motion {
|
||||
time: motion.time as u32,
|
||||
relative_x: motion.dx as f64,
|
||||
relative_y: motion.dy as f64,
|
||||
};
|
||||
if let Some(current_client) = current_client {
|
||||
event_tx
|
||||
.send((current_client, Event::Pointer(motion_event)))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
EiEvent::PointerMotionAbsolute(_) => {}
|
||||
EiEvent::Button(button) => {
|
||||
let button_event = PointerEvent::Button {
|
||||
time: button.time as u32,
|
||||
button: button.button,
|
||||
state: match button.state {
|
||||
ButtonState::Released => 0,
|
||||
ButtonState::Press => 1,
|
||||
},
|
||||
};
|
||||
if let Some(current_client) = current_client {
|
||||
event_tx
|
||||
.send((current_client, Event::Pointer(button_event)))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
EiEvent::ScrollDelta(_) => {}
|
||||
EiEvent::ScrollStop(_) => {}
|
||||
EiEvent::ScrollCancel(_) => {}
|
||||
EiEvent::ScrollDiscrete(scroll) => {
|
||||
if scroll.discrete_dy != 0 {
|
||||
let event = PointerEvent::Axis {
|
||||
time: 0,
|
||||
axis: 0,
|
||||
value: scroll.discrete_dy as f64,
|
||||
};
|
||||
if let Some(current_client) = current_client {
|
||||
event_tx
|
||||
.send((current_client, Event::Pointer(event)))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
if scroll.discrete_dx != 0 {
|
||||
let event = PointerEvent::Axis {
|
||||
time: 0,
|
||||
axis: 1,
|
||||
value: scroll.discrete_dx as f64,
|
||||
};
|
||||
if let Some(current_client) = current_client {
|
||||
event_tx
|
||||
.send((current_client, Event::Pointer(event)))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
};
|
||||
}
|
||||
EiEvent::KeyboardKey(key) => {
|
||||
let key_event = KeyboardEvent::Key {
|
||||
key: key.key,
|
||||
state: match key.state {
|
||||
KeyState::Press => 1,
|
||||
KeyState::Released => 0,
|
||||
},
|
||||
time: key.time as u32,
|
||||
};
|
||||
if let Some(current_client) = current_client {
|
||||
event_tx
|
||||
.send((current_client, Event::Keyboard(key_event)))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
EiEvent::TouchDown(_) => {}
|
||||
EiEvent::TouchUp(_) => {}
|
||||
EiEvent::TouchMotion(_) => {}
|
||||
EiEvent::Disconnected(d) => {
|
||||
log::error!("disconnect: {d:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> EventProducer for LibeiProducer<'a> {
|
||||
fn notify(&mut self, event: ClientEvent) -> io::Result<()> {
|
||||
let notify_tx = self.notify_tx.clone();
|
||||
tokio::task::spawn_local(async move {
|
||||
log::debug!("notifying {event:?}");
|
||||
let _ = notify_tx.send(ProducerEvent::ClientEvent(event)).await;
|
||||
log::debug!("done !");
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn release(&mut self) -> io::Result<()> {
|
||||
let notify_tx = self.notify_tx.clone();
|
||||
tokio::task::spawn_local(async move {
|
||||
log::debug!("notifying Release");
|
||||
let _ = notify_tx.send(ProducerEvent::Release).await;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Stream for LibeiProducer<'a> {
|
||||
type Item = io::Result<(ClientHandle, Event)>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
match ready!(self.event_rx.poll_recv(cx)) {
|
||||
None => Poll::Ready(None),
|
||||
Some(e) => Poll::Ready(Some(Ok(e))),
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/backend/producer/macos.rs
Normal file
33
src/backend/producer/macos.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
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) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn release(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -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, env};
|
||||
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,20 +23,26 @@ 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},
|
||||
xdg::xdg_output::zv1::client::{
|
||||
zxdg_output_manager_v1::ZxdgOutputManagerV1,
|
||||
zxdg_output_v1::{self, ZxdgOutputV1},
|
||||
},
|
||||
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}}};
|
||||
};
|
||||
|
||||
use wayland_protocols_wlr::layer_shell::v1::client::{
|
||||
zwlr_layer_shell_v1::{Layer, ZwlrLayerShellV1},
|
||||
@@ -33,14 +50,18 @@ 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_output::{self, WlOutput},
|
||||
wl_buffer, wl_compositor,
|
||||
wl_keyboard::{self, WlKeyboard},
|
||||
wl_output::{self, WlOutput},
|
||||
wl_pointer::{self, WlPointer},
|
||||
wl_region, wl_registry, wl_seat, wl_shm, wl_shm_pool,
|
||||
wl_surface::WlSurface,
|
||||
},
|
||||
Connection, Dispatch, DispatchError, QueueHandle, WEnum, EventQueue,
|
||||
Connection, Dispatch, DispatchError, EventQueue, QueueHandle, WEnum,
|
||||
};
|
||||
|
||||
use tempfile;
|
||||
@@ -55,7 +76,7 @@ struct Globals {
|
||||
seat: wl_seat::WlSeat,
|
||||
shm: wl_shm::WlShm,
|
||||
layer_shell: ZwlrLayerShellV1,
|
||||
outputs: Vec<wl_output::WlOutput>,
|
||||
outputs: Vec<WlOutput>,
|
||||
xdg_output_manager: ZxdgOutputManagerV1,
|
||||
}
|
||||
|
||||
@@ -70,39 +91,57 @@ impl OutputInfo {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
name: "".to_string(),
|
||||
position: (0,0),
|
||||
size: (0,0),
|
||||
position: (0, 0),
|
||||
size: (0, 0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct State {
|
||||
pointer: Option<WlPointer>,
|
||||
keyboard: Option<WlKeyboard>,
|
||||
pointer_lock: Option<ZwpLockedPointerV1>,
|
||||
rel_pointer: Option<ZwpRelativePointerV1>,
|
||||
shortcut_inhibitor: Option<ZwpKeyboardShortcutsInhibitorV1>,
|
||||
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,
|
||||
surface: WlSurface,
|
||||
layer_surface: ZwlrLayerSurfaceV1,
|
||||
pos: Position,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
fn new(state: &State, qh: &QueueHandle<State>, output: &WlOutput, pos: Position, size: (i32, i32)) -> Window {
|
||||
fn new(
|
||||
state: &State,
|
||||
qh: &QueueHandle<State>,
|
||||
output: &WlOutput,
|
||||
pos: Position,
|
||||
size: (i32, i32),
|
||||
) -> Window {
|
||||
log::debug!("creating window output: {output:?}, size: {size:?}");
|
||||
let g = &state.g;
|
||||
|
||||
let (width, height) = match pos {
|
||||
@@ -113,7 +152,7 @@ impl Window {
|
||||
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,
|
||||
@@ -127,7 +166,7 @@ impl Window {
|
||||
|
||||
let layer_surface = g.layer_shell.get_layer_surface(
|
||||
&surface,
|
||||
Some(&output),
|
||||
Some(output),
|
||||
Layer::Overlay,
|
||||
"LAN Mouse Sharing".into(),
|
||||
qh,
|
||||
@@ -143,10 +182,11 @@ impl Window {
|
||||
layer_surface.set_anchor(anchor);
|
||||
layer_surface.set_size(width, height);
|
||||
layer_surface.set_exclusive_zone(-1);
|
||||
layer_surface.set_margin(0,0,0,0);
|
||||
layer_surface.set_margin(0, 0, 0, 0);
|
||||
surface.set_input_region(None);
|
||||
surface.commit();
|
||||
Window {
|
||||
pos,
|
||||
buffer,
|
||||
surface,
|
||||
layer_surface,
|
||||
@@ -164,30 +204,39 @@ impl Drop for Window {
|
||||
}
|
||||
|
||||
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,
|
||||
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()
|
||||
.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);
|
||||
log::debug!("edges: {edges:?}");
|
||||
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).find(|e| e == edge).is_none()
|
||||
}).map(|(o,_)| o.clone()).collect();
|
||||
state.output_info
|
||||
let outputs: Vec<WlOutput> = edges
|
||||
.iter()
|
||||
.filter(|(o,_)| outputs.contains(o))
|
||||
.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()
|
||||
}
|
||||
@@ -198,7 +247,7 @@ fn draw(f: &mut File, (width, height): (u32, u32)) {
|
||||
for _ in 0..width {
|
||||
if env::var("LM_DEBUG_LAYER_SHELL").ok().is_some() {
|
||||
// AARRGGBB
|
||||
buf.write_all(&0xFF11d116u32.to_ne_bytes()).unwrap();
|
||||
buf.write_all(&0xff11d116u32.to_ne_bytes()).unwrap();
|
||||
} else {
|
||||
// AARRGGBB
|
||||
buf.write_all(&0x00000000u32.to_ne_bytes()).unwrap();
|
||||
@@ -256,10 +305,15 @@ 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![];
|
||||
|
||||
@@ -279,11 +333,13 @@ impl WaylandEventProducer {
|
||||
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 = 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);
|
||||
|
||||
let mut state = State {
|
||||
pointer: None,
|
||||
keyboard: None,
|
||||
g,
|
||||
pointer_lock: None,
|
||||
rel_pointer: None,
|
||||
@@ -293,7 +349,7 @@ impl WaylandEventProducer {
|
||||
qh,
|
||||
wayland_fd,
|
||||
read_guard: None,
|
||||
pending_events: vec![],
|
||||
pending_events: VecDeque::new(),
|
||||
output_info: vec![],
|
||||
};
|
||||
|
||||
@@ -307,7 +363,10 @@ impl WaylandEventProducer {
|
||||
|
||||
// read outputs
|
||||
for output in state.g.outputs.iter() {
|
||||
state.g.xdg_output_manager.get_xdg_output(output, &state.qh, output.clone());
|
||||
state
|
||||
.g
|
||||
.xdg_output_manager
|
||||
.get_xdg_output(output, &state.qh, output.clone());
|
||||
}
|
||||
|
||||
// roundtrip to read xdg_output events
|
||||
@@ -318,18 +377,46 @@ impl WaylandEventProducer {
|
||||
log::debug!("{:#?}", i.1);
|
||||
}
|
||||
|
||||
let read_guard = queue.prepare_read()?;
|
||||
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);
|
||||
|
||||
Ok(WaylandEventProducer { queue, state })
|
||||
let inner = AsyncFd::new(Inner { queue, state })?;
|
||||
|
||||
Ok(WaylandEventProducer(inner))
|
||||
}
|
||||
|
||||
fn add_client(&mut self, handle: ClientHandle, pos: Position) {
|
||||
self.0.get_mut().state.add_client(handle, pos);
|
||||
}
|
||||
|
||||
fn delete_client(&mut self, handle: ClientHandle) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn grab(
|
||||
&mut self,
|
||||
surface: &wl_surface::WlSurface,
|
||||
pointer: &wl_pointer::WlPointer,
|
||||
surface: &WlSurface,
|
||||
pointer: &WlPointer,
|
||||
serial: u32,
|
||||
qh: &QueueHandle<State>,
|
||||
) {
|
||||
@@ -409,42 +496,31 @@ impl State {
|
||||
}
|
||||
|
||||
fn add_client(&mut self, client: ClientHandle, pos: Position) {
|
||||
|
||||
let outputs = get_output_configuration(self, pos);
|
||||
|
||||
log::debug!("outputs: {outputs:?}");
|
||||
outputs.iter().for_each(|(o, i)| {
|
||||
let window = Window::new(&self, &self.qh, &o, pos, i.size);
|
||||
let window = Window::new(self, &self.qh, o, pos, i.size);
|
||||
let window = Rc::new(window);
|
||||
self.client_for_window.push((window, client));
|
||||
});
|
||||
}
|
||||
|
||||
fn update_windows(&mut self) {
|
||||
log::debug!("updating windows");
|
||||
log::debug!("output info: {:?}", self.output_info);
|
||||
let clients: Vec<_> = self
|
||||
.client_for_window
|
||||
.drain(..)
|
||||
.map(|(w, c)| (c, w.pos))
|
||||
.collect();
|
||||
for (client, pos) in clients {
|
||||
self.add_client(client, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -459,16 +535,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) {
|
||||
@@ -490,76 +570,101 @@ impl WaylandEventProducer {
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_events(&mut self) {
|
||||
fn flush_events(&mut self) -> io::Result<()> {
|
||||
// flush outgoing events
|
||||
match self.queue.flush() {
|
||||
Ok(_) => (),
|
||||
Err(e) => match e {
|
||||
WaylandError::Io(e) => {
|
||||
log::error!("error writing to wayland socket: {e}")
|
||||
},
|
||||
return Err(e);
|
||||
}
|
||||
WaylandError::Protocol(e) => {
|
||||
panic!("wayland protocol violation: {e}")
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
fn notify(&mut self, client_event: ClientEvent) -> io::Result<()> {
|
||||
match client_event {
|
||||
ClientEvent::Create(handle, pos) => {
|
||||
self.state.add_client(handle, pos);
|
||||
self.add_client(handle, pos);
|
||||
}
|
||||
ClientEvent::Destroy(handle) => {
|
||||
loop {
|
||||
// remove all windows corresponding to this client
|
||||
if let Some(i) = self
|
||||
.state
|
||||
.client_for_window
|
||||
.iter()
|
||||
.position(|(_,c)| *c == handle) {
|
||||
self.state.client_for_window.remove(i);
|
||||
self.state.focused = None;
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
self.delete_client(handle);
|
||||
}
|
||||
}
|
||||
self.flush_events();
|
||||
let inner = self.0.get_mut();
|
||||
inner.flush_events()
|
||||
}
|
||||
|
||||
fn release(&mut self) {
|
||||
self.state.ungrab();
|
||||
self.flush_events();
|
||||
fn release(&mut self) -> io::Result<()> {
|
||||
log::debug!("releasing pointer");
|
||||
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
|
||||
if let Err(e) = inner.flush_events() {
|
||||
if e.kind() != ErrorKind::WouldBlock {
|
||||
return Poll::Ready(Some(Err(e)));
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<wl_seat::WlSeat, ()> for State {
|
||||
fn event(
|
||||
_: &mut Self,
|
||||
state: &mut Self,
|
||||
seat: &wl_seat::WlSeat,
|
||||
event: <wl_seat::WlSeat as wayland_client::Proxy>::Event,
|
||||
_: &(),
|
||||
@@ -570,26 +675,25 @@ impl Dispatch<wl_seat::WlSeat, ()> for State {
|
||||
capabilities: WEnum::Value(capabilities),
|
||||
} = event
|
||||
{
|
||||
if capabilities.contains(wl_seat::Capability::Pointer) {
|
||||
seat.get_pointer(qh, ());
|
||||
if capabilities.contains(wl_seat::Capability::Pointer) && state.pointer.is_none() {
|
||||
state.pointer.replace(seat.get_pointer(qh, ()));
|
||||
}
|
||||
if capabilities.contains(wl_seat::Capability::Keyboard) {
|
||||
if capabilities.contains(wl_seat::Capability::Keyboard) && state.keyboard.is_none() {
|
||||
seat.get_keyboard(qh, ());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<wl_pointer::WlPointer, ()> for State {
|
||||
impl Dispatch<WlPointer, ()> for State {
|
||||
fn event(
|
||||
app: &mut Self,
|
||||
pointer: &wl_pointer::WlPointer,
|
||||
event: <wl_pointer::WlPointer as wayland_client::Proxy>::Event,
|
||||
pointer: &WlPointer,
|
||||
event: <WlPointer as wayland_client::Proxy>::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
) {
|
||||
|
||||
match event {
|
||||
wl_pointer::Event::Enter {
|
||||
serial,
|
||||
@@ -602,9 +706,10 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
|
||||
if let Some((window, client)) = app
|
||||
.client_for_window
|
||||
.iter()
|
||||
.find(|(w, _c)| w.surface == surface) {
|
||||
.find(|(w, _c)| w.surface == surface)
|
||||
{
|
||||
app.focused = Some((window.clone(), *client));
|
||||
app.grab(&surface, pointer, serial.clone(), qh);
|
||||
app.grab(&surface, pointer, serial, qh);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
@@ -614,9 +719,19 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
|
||||
.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 { .. } => {
|
||||
/* There are rare cases, where when a window is opened in
|
||||
* just the wrong moment, the pointer is released, while
|
||||
* still grabbed.
|
||||
* In that case, the pointer must be ungrabbed, otherwise
|
||||
* it is impossible to grab it again (since the pointer
|
||||
* lock, relative pointer,... objects are still in place)
|
||||
*/
|
||||
if app.pointer_lock.is_some() {
|
||||
log::warn!("compositor released mouse");
|
||||
}
|
||||
app.ungrab();
|
||||
}
|
||||
wl_pointer::Event::Button {
|
||||
@@ -626,7 +741,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
|
||||
state,
|
||||
} => {
|
||||
let (_, client) = app.focused.as_ref().unwrap();
|
||||
app.pending_events.push((
|
||||
app.pending_events.push_back((
|
||||
*client,
|
||||
Event::Pointer(PointerEvent::Button {
|
||||
time,
|
||||
@@ -637,7 +752,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
|
||||
}
|
||||
wl_pointer::Event::Axis { time, axis, value } => {
|
||||
let (_, client) = app.focused.as_ref().unwrap();
|
||||
app.pending_events.push((
|
||||
app.pending_events.push_back((
|
||||
*client,
|
||||
Event::Pointer(PointerEvent::Axis {
|
||||
time,
|
||||
@@ -656,10 +771,10 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<wl_keyboard::WlKeyboard, ()> for State {
|
||||
impl Dispatch<WlKeyboard, ()> for State {
|
||||
fn event(
|
||||
app: &mut Self,
|
||||
_: &wl_keyboard::WlKeyboard,
|
||||
_: &WlKeyboard,
|
||||
event: wl_keyboard::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
@@ -677,7 +792,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,
|
||||
@@ -695,7 +810,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,
|
||||
@@ -705,10 +820,6 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for State {
|
||||
}),
|
||||
));
|
||||
}
|
||||
if mods_depressed == 77 {
|
||||
// ctrl shift super alt
|
||||
app.ungrab();
|
||||
}
|
||||
}
|
||||
wl_keyboard::Event::Keymap {
|
||||
format: _,
|
||||
@@ -744,7 +855,7 @@ impl Dispatch<ZwpRelativePointerV1, ()> for State {
|
||||
{
|
||||
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,
|
||||
@@ -770,11 +881,12 @@ impl Dispatch<ZwlrLayerSurfaceV1, ()> for State {
|
||||
if let Some((window, _client)) = app
|
||||
.client_for_window
|
||||
.iter()
|
||||
.find(|(w, _c)| &w.layer_surface == layer_surface) {
|
||||
.find(|(w, _c)| &w.layer_surface == layer_surface)
|
||||
{
|
||||
// client corresponding to the layer_surface
|
||||
let surface = &window.surface;
|
||||
let buffer = &window.buffer;
|
||||
surface.attach(Some(&buffer), 0, 0);
|
||||
surface.attach(Some(buffer), 0, 0);
|
||||
layer_surface.ack_configure(serial);
|
||||
surface.commit();
|
||||
}
|
||||
@@ -805,17 +917,21 @@ impl Dispatch<wl_registry::WlRegistry, ()> for State {
|
||||
qh: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
wl_registry::Event::Global { name, interface, version: _ } => {
|
||||
match 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::Global {
|
||||
name,
|
||||
interface,
|
||||
version: _,
|
||||
} => {
|
||||
if interface.as_str() == "wl_output" {
|
||||
log::debug!("wl_output global");
|
||||
state
|
||||
.g
|
||||
.outputs
|
||||
.push(registry.bind::<WlOutput, _, _>(name, 4, qh, ()))
|
||||
}
|
||||
},
|
||||
wl_registry::Event::GlobalRemove { .. } => {},
|
||||
_ => {},
|
||||
}
|
||||
wl_registry::Event::GlobalRemove { .. } => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -830,11 +946,8 @@ impl Dispatch<ZxdgOutputV1, WlOutput> for State {
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
log::debug!("xdg-output - {event:?}");
|
||||
let output_info = match state
|
||||
.output_info
|
||||
.iter_mut()
|
||||
.find(|(o, _)| o == wl_output) {
|
||||
Some((_,c)) => c,
|
||||
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));
|
||||
@@ -845,16 +958,31 @@ impl Dispatch<ZxdgOutputV1, WlOutput> for State {
|
||||
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::Done => {}
|
||||
zxdg_output_v1::Event::Name { name } => {
|
||||
output_info.name = name;
|
||||
},
|
||||
zxdg_output_v1::Event::Description { .. } => {},
|
||||
_ => {},
|
||||
}
|
||||
zxdg_output_v1::Event::Description { .. } => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<WlOutput, ()> for State {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_proxy: &WlOutput,
|
||||
event: <WlOutput as wayland_client::Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
if let wl_output::Event::Done = event {
|
||||
state.update_windows();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -869,10 +997,9 @@ 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);
|
||||
delegate_noop!(State: ignore WlSurface);
|
||||
delegate_noop!(State: ignore ZwpKeyboardShortcutsInhibitorV1);
|
||||
delegate_noop!(State: ignore ZwpLockedPointerV1);
|
||||
|
||||
@@ -1,58 +1,35 @@
|
||||
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 read_events(&mut self) -> Drain<(ClientHandle, Event)> {
|
||||
self.pending_events.drain(..)
|
||||
fn notify(&mut self, _event: ClientEvent) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn release(&mut self) { }
|
||||
fn release(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
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,39 @@
|
||||
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, _event: ClientEvent) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn release(&mut self) {}
|
||||
fn release(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
132
src/client.rs
132
src/client.rs
@@ -1,6 +1,10 @@
|
||||
use std::{net::{SocketAddr, IpAddr}, collections::HashSet, fmt::Display, time::Instant};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fmt::Display,
|
||||
net::{IpAddr, SocketAddr},
|
||||
};
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum Position {
|
||||
@@ -29,12 +33,30 @@ impl Position {
|
||||
|
||||
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",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Position {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||
match s {
|
||||
"left" => Ok(Position::Left),
|
||||
"right" => Ok(Position::Right),
|
||||
"top" => Ok(Position::Top),
|
||||
"bottom" => Ok(Position::Bottom),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,24 +64,23 @@ impl Display for Position {
|
||||
pub struct 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,
|
||||
/// `active` address of the client, used to send data to.
|
||||
/// This should generally be the socket address where data
|
||||
/// was last received from.
|
||||
pub active_addr: Option<SocketAddr>,
|
||||
/// all socket addresses associated with a particular client
|
||||
/// all ip addresses associated with a particular client
|
||||
/// e.g. Laptops usually have at least an ethernet and a wifi port
|
||||
/// which have different ip addresses
|
||||
pub addrs: HashSet<SocketAddr>,
|
||||
pub ips: HashSet<IpAddr>,
|
||||
/// both active_addr and addrs can be None / empty so port needs to be stored seperately
|
||||
pub port: u16,
|
||||
/// position of a client on screen
|
||||
pub pos: Position,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum ClientEvent {
|
||||
Create(ClientHandle, Position),
|
||||
Destroy(ClientHandle),
|
||||
@@ -69,55 +90,67 @@ pub type ClientHandle = u32;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ClientState {
|
||||
/// information about the client
|
||||
pub client: Client,
|
||||
/// events should be sent to and received from the client
|
||||
pub active: bool,
|
||||
pub last_ping: Option<Instant>,
|
||||
pub last_seen: Option<Instant>,
|
||||
pub last_replied: Option<Instant>,
|
||||
/// `active` address of the client, used to send data to.
|
||||
/// This should generally be the socket address where data
|
||||
/// was last received from.
|
||||
pub active_addr: Option<SocketAddr>,
|
||||
/// tracks whether or not the client is responding to pings
|
||||
pub alive: bool,
|
||||
/// keys currently pressed by this client
|
||||
pub pressed_keys: HashSet<u32>,
|
||||
}
|
||||
|
||||
pub struct ClientManager {
|
||||
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![],
|
||||
}
|
||||
Self { clients: vec![] }
|
||||
}
|
||||
|
||||
/// add a new client to this manager
|
||||
pub fn add_client(
|
||||
&mut self,
|
||||
hostname: Option<String>,
|
||||
addrs: HashSet<IpAddr>,
|
||||
ips: HashSet<IpAddr>,
|
||||
port: u16,
|
||||
pos: Position,
|
||||
active: bool,
|
||||
) -> ClientHandle {
|
||||
// get a new client_handle
|
||||
let handle = self.free_id();
|
||||
|
||||
// we dont know, which IP is initially active
|
||||
let active_addr = None;
|
||||
|
||||
// map ip addresses to socket addresses
|
||||
let addrs = HashSet::from_iter(
|
||||
addrs
|
||||
.into_iter()
|
||||
.map(|ip| SocketAddr::new(ip, port))
|
||||
);
|
||||
// store fix ip addresses
|
||||
let fix_ips = ips.iter().cloned().collect();
|
||||
|
||||
// store the client
|
||||
let client = Client { hostname, handle, active_addr, addrs, port, pos };
|
||||
let client = Client {
|
||||
hostname,
|
||||
fix_ips,
|
||||
handle,
|
||||
ips,
|
||||
port,
|
||||
pos,
|
||||
};
|
||||
|
||||
// client was never seen, nor pinged
|
||||
let client_state = ClientState {
|
||||
client,
|
||||
last_ping: None,
|
||||
last_seen: None,
|
||||
last_replied: None,
|
||||
active: false,
|
||||
active,
|
||||
active_addr: None,
|
||||
alive: false,
|
||||
pressed_keys: HashSet::new(),
|
||||
};
|
||||
|
||||
if handle as usize >= self.clients.len() {
|
||||
@@ -135,10 +168,12 @@ impl ClientManager {
|
||||
// time this is likely faster than using a HashMap
|
||||
self.clients
|
||||
.iter()
|
||||
.position(|c| if let Some(c) = c {
|
||||
c.active && c.client.addrs.contains(&addr)
|
||||
} else {
|
||||
false
|
||||
.position(|c| {
|
||||
if let Some(c) = c {
|
||||
c.active && c.client.ips.contains(&addr.ip())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.map(|p| p as ClientHandle)
|
||||
}
|
||||
@@ -153,7 +188,8 @@ impl ClientManager {
|
||||
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() {
|
||||
|| self.clients.get(i as usize).unwrap().is_none()
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
@@ -161,20 +197,20 @@ impl ClientManager {
|
||||
}
|
||||
|
||||
// returns an immutable reference to the client state corresponding to `client`
|
||||
pub fn get<'a>(&'a self, client: ClientHandle) -> Option<&'a ClientState> {
|
||||
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<'a>(&'a mut self, client: ClientHandle) -> Option<&'a mut ClientState> {
|
||||
pub fn get_mut(&mut self, client: ClientHandle) -> Option<&mut ClientState> {
|
||||
self.clients.get_mut(client as usize)?.as_mut()
|
||||
}
|
||||
|
||||
pub fn enumerate(&self) -> Vec<(Client, bool)> {
|
||||
self.clients
|
||||
.iter()
|
||||
.filter_map(|s|s.as_ref())
|
||||
.map(|s| (s.client.clone(), s.active))
|
||||
.collect()
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
179
src/config.rs
179
src/config.rs
@@ -1,13 +1,15 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use core::fmt;
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::net::IpAddr;
|
||||
use std::{error::Error, fs};
|
||||
|
||||
use std::env;
|
||||
use toml;
|
||||
|
||||
use crate::client::Position;
|
||||
use crate::scancode;
|
||||
use crate::scancode::Linux::{KeyLeftAlt, KeyLeftCtrl, KeyLeftMeta, KeyLeftShift};
|
||||
|
||||
pub const DEFAULT_PORT: u16 = 4242;
|
||||
|
||||
@@ -15,79 +17,107 @@ pub const DEFAULT_PORT: u16 = 4242;
|
||||
pub struct ConfigToml {
|
||||
pub port: Option<u16>,
|
||||
pub frontend: Option<String>,
|
||||
pub left: Option<Client>,
|
||||
pub right: Option<Client>,
|
||||
pub top: Option<Client>,
|
||||
pub bottom: Option<Client>,
|
||||
pub release_bind: Option<Vec<scancode::Linux>>,
|
||||
pub left: Option<TomlClient>,
|
||||
pub right: Option<TomlClient>,
|
||||
pub top: Option<TomlClient>,
|
||||
pub bottom: Option<TomlClient>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
pub struct Client {
|
||||
pub struct TomlClient {
|
||||
pub hostname: Option<String>,
|
||||
pub host_name: Option<String>,
|
||||
pub ips: Option<Vec<IpAddr>>,
|
||||
pub port: Option<u16>,
|
||||
pub activate_on_startup: Option<bool>,
|
||||
}
|
||||
|
||||
#[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(PartialEq, Eq)]
|
||||
#[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 clients: Vec<(TomlClient, Position)>,
|
||||
pub daemon: bool,
|
||||
pub release_bind: Vec<scancode::Linux>,
|
||||
}
|
||||
|
||||
pub struct ConfigClient {
|
||||
pub ips: HashSet<IpAddr>,
|
||||
pub hostname: Option<String>,
|
||||
pub port: u16,
|
||||
pub pos: Position,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
const DEFAULT_RELEASE_KEYS: [scancode::Linux; 4] =
|
||||
[KeyLeftCtrl, KeyLeftShift, KeyLeftMeta, KeyLeftAlt];
|
||||
|
||||
impl Config {
|
||||
pub fn new() -> Result<Self, 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::warn!("{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,26 +126,32 @@ impl Config {
|
||||
};
|
||||
|
||||
let frontend = match frontend {
|
||||
#[cfg(all(unix, feature = "gtk"))]
|
||||
#[cfg(feature = "gtk")]
|
||||
None => Frontend::Gtk,
|
||||
#[cfg(any(not(feature = "gtk"), not(unix)))]
|
||||
#[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![];
|
||||
log::debug!("{config_toml:?}");
|
||||
let release_bind = config_toml
|
||||
.as_ref()
|
||||
.and_then(|c| c.release_bind.clone())
|
||||
.unwrap_or(Vec::from_iter(DEFAULT_RELEASE_KEYS.iter().cloned()));
|
||||
|
||||
let mut clients: Vec<(TomlClient, Position)> = vec![];
|
||||
|
||||
if let Some(config_toml) = config_toml {
|
||||
if let Some(c) = config_toml.right {
|
||||
@@ -132,19 +168,40 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Config { frontend, clients, port })
|
||||
let daemon = args.daemon;
|
||||
|
||||
Ok(Config {
|
||||
daemon,
|
||||
frontend,
|
||||
clients,
|
||||
port,
|
||||
release_bind,
|
||||
})
|
||||
}
|
||||
|
||||
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()
|
||||
pub fn get_clients(&self) -> Vec<ConfigClient> {
|
||||
self.clients
|
||||
.iter()
|
||||
.map(|(c, pos)| {
|
||||
let port = c.port.unwrap_or(DEFAULT_PORT);
|
||||
let ips: HashSet<IpAddr> = if let Some(ips) = c.ips.as_ref() {
|
||||
HashSet::from_iter(ips.iter().cloned())
|
||||
} else {
|
||||
HashSet::new()
|
||||
};
|
||||
let hostname = match &c.hostname {
|
||||
Some(h) => Some(h.clone()),
|
||||
None => c.host_name.clone(),
|
||||
};
|
||||
let active = c.activate_on_startup.unwrap_or(false);
|
||||
ConfigClient {
|
||||
ips,
|
||||
hostname,
|
||||
port,
|
||||
pos: *pos,
|
||||
active,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
17
src/dns.rs
17
src/dns.rs
@@ -1,20 +1,23 @@
|
||||
use anyhow::Result;
|
||||
use std::{error::Error, net::IpAddr};
|
||||
|
||||
use trust_dns_resolver::Resolver;
|
||||
use trust_dns_resolver::TokioAsyncResolver;
|
||||
|
||||
pub(crate) struct DnsResolver {
|
||||
resolver: Resolver,
|
||||
pub struct DnsResolver {
|
||||
resolver: TokioAsyncResolver,
|
||||
}
|
||||
impl DnsResolver {
|
||||
pub(crate) fn new() -> Result<Self> {
|
||||
let resolver = Resolver::from_system_conf()?;
|
||||
pub(crate) async fn new() -> Result<Self> {
|
||||
let resolver = TokioAsyncResolver::tokio_from_system_conf()?;
|
||||
Ok(Self { resolver })
|
||||
}
|
||||
|
||||
pub(crate) fn resolve(&self, host: &str) -> Result<Vec<IpAddr>, Box<dyn Error>> {
|
||||
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)?;
|
||||
let response = self.resolver.lookup_ip(host).await?;
|
||||
for ip in response.iter() {
|
||||
log::info!("{host}: adding ip {ip}");
|
||||
}
|
||||
Ok(response.iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
267
src/event.rs
267
src/event.rs
@@ -1,8 +1,17 @@
|
||||
use std::{error::Error, fmt::{self, Display}};
|
||||
use anyhow::{anyhow, Result};
|
||||
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)]
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
pub enum PointerEvent {
|
||||
Motion {
|
||||
time: u32,
|
||||
@@ -22,7 +31,7 @@ pub enum PointerEvent {
|
||||
Frame {},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
pub enum KeyboardEvent {
|
||||
Key {
|
||||
time: u32,
|
||||
@@ -37,22 +46,50 @@ pub enum KeyboardEvent {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(PartialEq, 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(),
|
||||
/// explicit disconnect request. The client will no longer
|
||||
/// send events until the next Enter event. All of its keys should be released.
|
||||
Disconnect(),
|
||||
}
|
||||
|
||||
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 +97,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,9 +120,11 @@ 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"),
|
||||
Event::Disconnect() => write!(f, "disconnect"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,11 +132,13 @@ 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,
|
||||
Self::Disconnect() => EventType::Disconnect,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,10 +146,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,40 +157,42 @@ 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,
|
||||
Disconnect,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for PointerEventType {
|
||||
type Error = Box<dyn Error>;
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
fn try_from(value: u8) -> Result<Self> {
|
||||
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),
|
||||
_ => Err(Box::new(ProtocolError {
|
||||
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(anyhow!(ProtocolError {
|
||||
msg: format!("invalid pointer event type {}", value),
|
||||
})),
|
||||
}
|
||||
@@ -145,30 +200,32 @@ impl TryFrom<u8> for PointerEventType {
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for KeyboardEventType {
|
||||
type Error = Box<dyn Error>;
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
fn try_from(value: u8) -> Result<Self> {
|
||||
match value {
|
||||
x if x == Self::KEY as u8 => Ok(Self::KEY),
|
||||
x if x == Self::MODIFIERS as u8 => Ok(Self::MODIFIERS),
|
||||
_ => Err(Box::new(ProtocolError {
|
||||
x if x == Self::Key as u8 => Ok(Self::Key),
|
||||
x if x == Self::Modifiers as u8 => Ok(Self::Modifiers),
|
||||
_ => Err(anyhow!(ProtocolError {
|
||||
msg: format!("invalid keyboard event type {}", value),
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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![],
|
||||
Event::Disconnect() => vec![],
|
||||
};
|
||||
vec![event_id, event_data].concat()
|
||||
[event_id, event_data].concat()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,27 +242,29 @@ impl fmt::Display for ProtocolError {
|
||||
impl Error for ProtocolError {}
|
||||
|
||||
impl TryFrom<Vec<u8>> for Event {
|
||||
type Error = Box<dyn Error>;
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
|
||||
fn try_from(value: Vec<u8>) -> Result<Self> {
|
||||
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()),
|
||||
_ => Err(Box::new(ProtocolError {
|
||||
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()),
|
||||
i if i == (EventType::Disconnect as u8) => Ok(Event::Disconnect()),
|
||||
_ => Err(anyhow!(ProtocolError {
|
||||
msg: format!("invalid event_id {}", event_id),
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 +273,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,26 +283,26 @@ 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()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Vec<u8>> for PointerEvent {
|
||||
type Error = Box<dyn Error>;
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
|
||||
fn try_from(data: Vec<u8>) -> Result<Self> {
|
||||
match data.get(1) {
|
||||
Some(id) => {
|
||||
let event_type = match id.to_owned().try_into() {
|
||||
@@ -251,11 +310,11 @@ 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 => {
|
||||
return Err(Box::new(ProtocolError {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 4 Bytes at index 2".into(),
|
||||
}))
|
||||
}
|
||||
@@ -263,7 +322,7 @@ impl TryFrom<Vec<u8>> for PointerEvent {
|
||||
let relative_x = match data.get(6..14) {
|
||||
Some(d) => f64::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
return Err(Box::new(ProtocolError {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 8 Bytes at index 6".into(),
|
||||
}))
|
||||
}
|
||||
@@ -271,7 +330,7 @@ impl TryFrom<Vec<u8>> for PointerEvent {
|
||||
let relative_y = match data.get(14..22) {
|
||||
Some(d) => f64::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
return Err(Box::new(ProtocolError {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 8 Bytes at index 14".into(),
|
||||
}))
|
||||
}
|
||||
@@ -282,11 +341,11 @@ 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 => {
|
||||
return Err(Box::new(ProtocolError {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 4 Bytes at index 2".into(),
|
||||
}))
|
||||
}
|
||||
@@ -294,7 +353,7 @@ impl TryFrom<Vec<u8>> for PointerEvent {
|
||||
let button = match data.get(6..10) {
|
||||
Some(d) => u32::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
return Err(Box::new(ProtocolError {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 4 Bytes at index 10".into(),
|
||||
}))
|
||||
}
|
||||
@@ -302,7 +361,7 @@ impl TryFrom<Vec<u8>> for PointerEvent {
|
||||
let state = match data.get(10..14) {
|
||||
Some(d) => u32::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
return Err(Box::new(ProtocolError {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 4 Bytes at index 14".into(),
|
||||
}))
|
||||
}
|
||||
@@ -313,11 +372,11 @@ 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 => {
|
||||
return Err(Box::new(ProtocolError {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 4 Bytes at index 2".into(),
|
||||
}))
|
||||
}
|
||||
@@ -325,7 +384,7 @@ impl TryFrom<Vec<u8>> for PointerEvent {
|
||||
let axis = match data.get(6) {
|
||||
Some(d) => *d,
|
||||
None => {
|
||||
return Err(Box::new(ProtocolError {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 1 Byte at index 6".into(),
|
||||
}));
|
||||
}
|
||||
@@ -333,32 +392,32 @@ impl TryFrom<Vec<u8>> for PointerEvent {
|
||||
let value = match data.get(7..15) {
|
||||
Some(d) => f64::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
return Err(Box::new(ProtocolError {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 8 Bytes at index 7".into(),
|
||||
}));
|
||||
}
|
||||
};
|
||||
Ok(Self::Axis { time, axis, value })
|
||||
}
|
||||
PointerEventType::FRAME => Ok(Self::Frame {}),
|
||||
PointerEventType::Frame => Ok(Self::Frame {}),
|
||||
}
|
||||
}
|
||||
None => Err(Box::new(ProtocolError {
|
||||
None => Err(anyhow!(ProtocolError {
|
||||
msg: "Expected an element at index 0".into(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 +429,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,14 +438,14 @@ impl Into<Vec<u8>> for &KeyboardEvent {
|
||||
.concat()
|
||||
}
|
||||
};
|
||||
vec![id, data].concat()
|
||||
[id, data].concat()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Vec<u8>> for KeyboardEvent {
|
||||
type Error = Box<dyn Error>;
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
|
||||
fn try_from(data: Vec<u8>) -> Result<Self> {
|
||||
match data.get(1) {
|
||||
Some(id) => {
|
||||
let event_type = match id.to_owned().try_into() {
|
||||
@@ -394,11 +453,11 @@ 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 => {
|
||||
return Err(Box::new(ProtocolError {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 4 Bytes at index 6".into(),
|
||||
}))
|
||||
}
|
||||
@@ -406,7 +465,7 @@ impl TryFrom<Vec<u8>> for KeyboardEvent {
|
||||
let key = match data.get(6..10) {
|
||||
Some(d) => u32::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
return Err(Box::new(ProtocolError {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 4 Bytes at index 10".into(),
|
||||
}))
|
||||
}
|
||||
@@ -414,18 +473,18 @@ impl TryFrom<Vec<u8>> for KeyboardEvent {
|
||||
let state = match data.get(10) {
|
||||
Some(d) => *d,
|
||||
None => {
|
||||
return Err(Box::new(ProtocolError {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 1 Bytes at index 14".into(),
|
||||
}))
|
||||
}
|
||||
};
|
||||
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 => {
|
||||
return Err(Box::new(ProtocolError {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 4 Bytes at index 6".into(),
|
||||
}))
|
||||
}
|
||||
@@ -433,7 +492,7 @@ impl TryFrom<Vec<u8>> for KeyboardEvent {
|
||||
let mods_latched = match data.get(6..10) {
|
||||
Some(d) => u32::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
return Err(Box::new(ProtocolError {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 4 Bytes at index 10".into(),
|
||||
}))
|
||||
}
|
||||
@@ -441,7 +500,7 @@ impl TryFrom<Vec<u8>> for KeyboardEvent {
|
||||
let mods_locked = match data.get(10..14) {
|
||||
Some(d) => u32::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
return Err(Box::new(ProtocolError {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 4 Bytes at index 14".into(),
|
||||
}))
|
||||
}
|
||||
@@ -449,7 +508,7 @@ impl TryFrom<Vec<u8>> for KeyboardEvent {
|
||||
let group = match data.get(14..18) {
|
||||
Some(d) => u32::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
return Err(Box::new(ProtocolError {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 4 Bytes at index 18".into(),
|
||||
}))
|
||||
}
|
||||
@@ -463,7 +522,7 @@ impl TryFrom<Vec<u8>> for KeyboardEvent {
|
||||
}
|
||||
}
|
||||
}
|
||||
None => Err(Box::new(ProtocolError {
|
||||
None => Err(anyhow!(ProtocolError {
|
||||
msg: "Expected an element at index 0".into(),
|
||||
})),
|
||||
}
|
||||
|
||||
@@ -1,465 +0,0 @@
|
||||
use std::{error::Error, io::Result, collections::HashSet, time::{Duration, Instant}, net::IpAddr};
|
||||
use log;
|
||||
use mio::{Events, Poll, Interest, Token, net::UdpSocket, event::Source};
|
||||
#[cfg(not(windows))]
|
||||
use mio_signals::{Signals, Signal, SignalSet};
|
||||
|
||||
use std::{net::SocketAddr, io::ErrorKind};
|
||||
|
||||
use crate::{client::{ClientEvent, ClientManager, Position, ClientHandle}, consumer::EventConsumer, producer::EventProducer, frontend::{FrontendEvent, FrontendListener, FrontendNotify}, dns::{self, DnsResolver}};
|
||||
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>,
|
||||
resolver: DnsResolver,
|
||||
#[cfg(not(windows))]
|
||||
signals: Signals,
|
||||
frontend: FrontendListener,
|
||||
client_manager: ClientManager,
|
||||
state: State,
|
||||
next_token: usize,
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
const MAX_TOKEN: usize = 4;
|
||||
|
||||
impl Server {
|
||||
pub fn new(
|
||||
port: u16,
|
||||
mut producer: Box<dyn EventProducer>,
|
||||
consumer: Box<dyn EventConsumer>,
|
||||
mut frontend: FrontendListener,
|
||||
) -> anyhow::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)?;
|
||||
|
||||
// create dns resolver
|
||||
let resolver = dns::DnsResolver::new()?;
|
||||
|
||||
// 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)?;
|
||||
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,
|
||||
resolver,
|
||||
#[cfg(not(windows))]
|
||||
signals, frontend,
|
||||
client_manager,
|
||||
state: State::Receiving,
|
||||
next_token: MAX_TOKEN,
|
||||
})
|
||||
}
|
||||
|
||||
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 => self.handle_frontend_incoming(),
|
||||
#[cfg(not(windows))]
|
||||
SIGNAL => if self.handle_signal() { return Ok(()) },
|
||||
_ => if self.handle_frontend_event(event.token()) { return Ok(()) },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_client(&mut self, hostname: Option<String>, mut addr: HashSet<IpAddr>, port: u16, pos: Position) -> ClientHandle {
|
||||
let ips = if let Some(hostname) = hostname.as_ref() {
|
||||
HashSet::from_iter(self.resolver.resolve(hostname.as_str()).ok().iter().flatten().cloned())
|
||||
} else {
|
||||
HashSet::new()
|
||||
};
|
||||
addr.extend(ips.iter());
|
||||
log::info!("adding client [{}]{} @ {:?}", pos, hostname.as_deref().unwrap_or(""), &ips);
|
||||
let client = self.client_manager.add_client(hostname.clone(), addr, port, pos);
|
||||
log::debug!("add_client {client}");
|
||||
let notify = FrontendNotify::NotifyClientCreate(client, hostname, port, pos);
|
||||
if let Err(e) = self.frontend.notify_all(notify) {
|
||||
log::error!("{e}");
|
||||
};
|
||||
client
|
||||
}
|
||||
|
||||
pub fn activate_client(&mut self, client: ClientHandle, active: bool) {
|
||||
if let Some(state) = self.client_manager.get_mut(client) {
|
||||
state.active = active;
|
||||
if state.active {
|
||||
self.producer.notify(ClientEvent::Create(client, state.client.pos));
|
||||
self.consumer.notify(ClientEvent::Create(client, state.client.pos));
|
||||
} else {
|
||||
self.producer.notify(ClientEvent::Destroy(client));
|
||||
self.consumer.notify(ClientEvent::Destroy(client));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_client(&mut self, client: ClientHandle) -> Option<ClientHandle> {
|
||||
self.producer.notify(ClientEvent::Destroy(client));
|
||||
self.consumer.notify(ClientEvent::Destroy(client));
|
||||
if let Some(client) = self.client_manager.remove_client(client).map(|s| s.client.handle) {
|
||||
let notify = FrontendNotify::NotifyClientDelete(client);
|
||||
log::debug!("{notify:?}");
|
||||
if let Err(e) = self.frontend.notify_all(notify) {
|
||||
log::error!("{e}");
|
||||
}
|
||||
Some(client)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_client(
|
||||
&mut self,
|
||||
client: ClientHandle,
|
||||
hostname: Option<String>,
|
||||
port: u16,
|
||||
pos: Position,
|
||||
) {
|
||||
// retrieve state
|
||||
let Some(state) = self.client_manager.get_mut(client) else {
|
||||
return
|
||||
};
|
||||
|
||||
// update pos
|
||||
state.client.pos = pos;
|
||||
if state.active {
|
||||
self.producer.notify(ClientEvent::Destroy(client));
|
||||
self.consumer.notify(ClientEvent::Destroy(client));
|
||||
self.producer.notify(ClientEvent::Create(client, pos));
|
||||
self.consumer.notify(ClientEvent::Create(client, pos));
|
||||
}
|
||||
|
||||
// update port
|
||||
if state.client.port != port {
|
||||
state.client.port = port;
|
||||
state.client.addrs = state.client.addrs
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|mut a| { a.set_port(port); a })
|
||||
.collect();
|
||||
state.client.active_addr.map(|a| { SocketAddr::new(a.ip(), port) });
|
||||
}
|
||||
|
||||
// update hostname
|
||||
if state.client.hostname != hostname {
|
||||
state.client.addrs = HashSet::new();
|
||||
state.client.active_addr = None;
|
||||
state.client.hostname = hostname;
|
||||
if let Some(hostname) = state.client.hostname.as_ref() {
|
||||
if let Ok(ips) = self.resolver.resolve(hostname.as_str()) {
|
||||
let addrs = ips.iter().map(|i| SocketAddr::new(*i, port));
|
||||
state.client.addrs = HashSet::from_iter(addrs);
|
||||
}
|
||||
}
|
||||
}
|
||||
log::debug!("client updated: {:?}", state);
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
};
|
||||
|
||||
// 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
|
||||
}
|
||||
};
|
||||
log::trace!("{:20} <-<-<-<------ {addr} ({handle})", event.to_string());
|
||||
let state = match self.client_manager.get_mut(handle) {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
log::error!("unknown handle");
|
||||
continue
|
||||
}
|
||||
};
|
||||
|
||||
// reset ttl for client and
|
||||
state.last_seen = Some(Instant::now());
|
||||
// set addr as new default for this client
|
||||
state.client.active_addr = Some(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);
|
||||
}
|
||||
// we release the mouse here,
|
||||
// since its very likely, that we wont get a release event
|
||||
self.producer.release();
|
||||
}
|
||||
(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 = state.last_replied;
|
||||
if last_replied.is_none()
|
||||
|| last_replied.is_some()
|
||||
&& last_replied.unwrap().elapsed() > Duration::from_secs(1) {
|
||||
state.last_replied = Some(Instant::now());
|
||||
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();
|
||||
let mut should_release = false;
|
||||
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;
|
||||
}
|
||||
|
||||
log::trace!("producer: ({c}) {e:?}");
|
||||
let state = match self.client_manager.get_mut(c) {
|
||||
Some(state) => state,
|
||||
None => {
|
||||
log::warn!("unknown client!");
|
||||
continue
|
||||
}
|
||||
};
|
||||
// otherwise we should have an address to send to
|
||||
// transmit events to the corrensponding client
|
||||
if let Some(addr) = state.client.active_addr {
|
||||
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
|
||||
if state.last_seen.is_some()
|
||||
&& state.last_seen.unwrap().elapsed() < Duration::from_secs(2) {
|
||||
continue
|
||||
}
|
||||
|
||||
// client last seen > 500ms ago
|
||||
if state.last_ping.is_some()
|
||||
&& state.last_ping.unwrap().elapsed() < Duration::from_millis(500) {
|
||||
continue
|
||||
}
|
||||
|
||||
// release mouse if client didnt respond to the first ping
|
||||
if state.last_ping.is_some()
|
||||
&& state.last_ping.unwrap().elapsed() < Duration::from_secs(1) {
|
||||
should_release = true;
|
||||
}
|
||||
|
||||
// last ping > 500ms ago -> ping all interfaces
|
||||
state.last_ping = Some(Instant::now());
|
||||
for addr in state.client.addrs.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);
|
||||
}
|
||||
}
|
||||
// send additional release event, in case client is still in sending mode
|
||||
if let Err(e) = Self::send_event(&self.socket, Event::Release(), *addr) {
|
||||
if e.kind() != ErrorKind::WouldBlock {
|
||||
log::error!("udp send: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if should_release && self.state != State::Receiving {
|
||||
log::info!("client not responding - releasing pointer");
|
||||
self.producer.release();
|
||||
self.state = State::Receiving;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn handle_frontend_incoming(&mut self) {
|
||||
loop {
|
||||
let token = self.fresh_token();
|
||||
let poll = &mut self.poll;
|
||||
match self.frontend.handle_incoming(|s, i| {
|
||||
poll.registry().register(s, token, i)?;
|
||||
Ok(token)
|
||||
}) {
|
||||
Err(e) if e.kind() == ErrorKind::WouldBlock => break,
|
||||
Err(e) if e.kind() == ErrorKind::Interrupted => continue,
|
||||
Err(e) => {
|
||||
log::error!("{e}");
|
||||
break
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
// notify new frontend connections of current clients
|
||||
self.enumerate();
|
||||
}
|
||||
|
||||
fn handle_frontend_event(&mut self, token: Token) -> bool {
|
||||
loop {
|
||||
let event = match self.frontend.read_event(token) {
|
||||
Ok(event) => event,
|
||||
Err(e) if e.kind() == ErrorKind::WouldBlock => return false,
|
||||
Err(e) if e.kind() == ErrorKind::Interrupted => continue,
|
||||
Err(e) => {
|
||||
log::error!("{e}");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
if let Some(event) = event {
|
||||
log::debug!("frontend: {event:?}");
|
||||
match event {
|
||||
FrontendEvent::AddClient(hostname, port, pos) => {
|
||||
self.add_client(hostname, HashSet::new(), port, pos);
|
||||
}
|
||||
FrontendEvent::ActivateClient(client, active) => {
|
||||
self.activate_client(client, active);
|
||||
}
|
||||
FrontendEvent::DelClient(client) => {
|
||||
self.remove_client(client);
|
||||
}
|
||||
FrontendEvent::UpdateClient(client, hostname, port, pos) => {
|
||||
self.update_client(client, hostname, port, pos);
|
||||
}
|
||||
FrontendEvent::Enumerate() => self.enumerate(),
|
||||
FrontendEvent::Shutdown() => {
|
||||
log::info!("terminating gracefully...");
|
||||
return true;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn enumerate(&mut self) {
|
||||
let clients = self.client_manager.enumerate();
|
||||
if let Err(e) = self.frontend.notify_all(FrontendNotify::Enumerate(clients)) {
|
||||
log::error!("{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> {
|
||||
log::trace!("{:20} ------>->->-> {addr}", e.to_string());
|
||||
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)),
|
||||
}
|
||||
}
|
||||
|
||||
fn fresh_token(&mut self) -> Token {
|
||||
let token = self.next_token as usize;
|
||||
self.next_token += 1;
|
||||
Token(token)
|
||||
}
|
||||
|
||||
pub fn register_frontend(&mut self, source: &mut dyn Source, interests: Interest) -> Result<Token> {
|
||||
let token = self.fresh_token();
|
||||
self.poll.registry().register(source, token, interests)?;
|
||||
Ok(token)
|
||||
}
|
||||
}
|
||||
332
src/frontend.rs
332
src/frontend.rs
@@ -1,54 +1,115 @@
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Read, Result, Write};
|
||||
use std::str;
|
||||
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::Interest;
|
||||
use mio::{Registry, Token, event::Source};
|
||||
use tokio::io::ReadHalf;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, WriteHalf};
|
||||
|
||||
#[cfg(unix)]
|
||||
use mio::net::UnixStream;
|
||||
use tokio::net::UnixListener;
|
||||
#[cfg(unix)]
|
||||
use mio::net::UnixListener;
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
#[cfg(windows)]
|
||||
use mio::net::TcpStream;
|
||||
use tokio::net::TcpListener;
|
||||
#[cfg(windows)]
|
||||
use mio::net::TcpListener;
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::client::{Position, ClientHandle, Client};
|
||||
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 {
|
||||
/// add a new client
|
||||
AddClient(Option<String>, u16, Position),
|
||||
/// activate/deactivate client
|
||||
ActivateClient(ClientHandle, bool),
|
||||
/// update a client (hostname, port, position)
|
||||
UpdateClient(ClientHandle, Option<String>, u16, Position),
|
||||
/// 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(ClientHandle, Option<String>, u16, Position),
|
||||
NotifyClientUpdate(ClientHandle, Option<String>, u16, Position),
|
||||
NotifyClientActivate(ClientHandle, bool),
|
||||
NotifyClientCreate(Client),
|
||||
NotifyClientUpdate(Client),
|
||||
NotifyClientDelete(ClientHandle),
|
||||
/// new port, reason of failure (if failed)
|
||||
NotifyPortChange(u16, Option<String>),
|
||||
/// Client State, active
|
||||
Enumerate(Vec<(Client, bool)>),
|
||||
NotifyError(String),
|
||||
}
|
||||
@@ -60,103 +121,131 @@ pub struct FrontendListener {
|
||||
listener: UnixListener,
|
||||
#[cfg(unix)]
|
||||
socket_path: PathBuf,
|
||||
frontend_connections: HashMap<Token, FrontendConnection>,
|
||||
#[cfg(unix)]
|
||||
tx_streams: Vec<WriteHalf<UnixStream>>,
|
||||
#[cfg(windows)]
|
||||
tx_streams: Vec<WriteHalf<TcpStream>>,
|
||||
}
|
||||
|
||||
impl FrontendListener {
|
||||
pub fn new() -> std::result::Result<Self, Box<dyn std::error::Error>> {
|
||||
#[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)]
|
||||
log::debug!("remove socket: {:?}", socket_path);
|
||||
#[cfg(unix)]
|
||||
if socket_path.exists() {
|
||||
std::fs::remove_file(&socket_path).unwrap();
|
||||
}
|
||||
#[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,
|
||||
frontend_connections: HashMap::new(),
|
||||
tx_streams: vec![],
|
||||
};
|
||||
|
||||
Ok(adapter)
|
||||
Some(Ok(adapter))
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn handle_incoming<F>(&mut self, register_frontend: F) -> Result<()>
|
||||
where F: Fn(&mut UnixStream, Interest) -> Result<Token> {
|
||||
let (mut stream, _) = self.listener.accept()?;
|
||||
let token = register_frontend(&mut stream, Interest::READABLE)?;
|
||||
let con = FrontendConnection::new(stream);
|
||||
self.frontend_connections.insert(token, con);
|
||||
Ok(())
|
||||
pub async fn accept(&mut self) -> Result<ReadHalf<UnixStream>> {
|
||||
let stream = self.listener.accept().await?.0;
|
||||
let (rx, tx) = tokio::io::split(stream);
|
||||
self.tx_streams.push(tx);
|
||||
Ok(rx)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn handle_incoming<F>(&mut self, register_frontend: F) -> Result<()>
|
||||
where F: Fn(&mut TcpStream, Interest) -> Result<Token> {
|
||||
let (mut stream, _) = self.listener.accept()?;
|
||||
let token = register_frontend(&mut stream, Interest::READABLE)?;
|
||||
let con = FrontendConnection::new(stream);
|
||||
self.frontend_connections.insert(token, con);
|
||||
Ok(())
|
||||
pub async fn accept(&mut self) -> Result<ReadHalf<TcpStream>> {
|
||||
let stream = self.listener.accept().await?.0;
|
||||
let (rx, tx) = tokio::io::split(stream);
|
||||
self.tx_streams.push(tx);
|
||||
Ok(rx)
|
||||
}
|
||||
|
||||
pub fn read_event(&mut self, token: Token) -> Result<Option<FrontendEvent>> {
|
||||
if let Some(con) = self.frontend_connections.get_mut(&token) {
|
||||
con.handle_event()
|
||||
} else {
|
||||
panic!("unknown token");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn notify_all(&mut self, notify: FrontendNotify) -> Result<()> {
|
||||
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_ne_bytes();
|
||||
let len = payload.len().to_be_bytes();
|
||||
log::debug!("json: {json}, len: {}", payload.len());
|
||||
|
||||
for con in self.frontend_connections.values_mut() {
|
||||
let mut keep = vec![];
|
||||
// TODO do simultaneously
|
||||
for tx in self.tx_streams.iter_mut() {
|
||||
// write len + payload
|
||||
con.stream.write(&len)?;
|
||||
con.stream.write(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(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Source for FrontendListener {
|
||||
fn register(
|
||||
&mut self,
|
||||
registry: &Registry,
|
||||
token: Token,
|
||||
interests: mio::Interest,
|
||||
) -> Result<()> {
|
||||
self.listener.register(registry, token, interests)
|
||||
}
|
||||
|
||||
fn reregister(
|
||||
&mut self,
|
||||
registry: &Registry,
|
||||
token: Token,
|
||||
interests: mio::Interest,
|
||||
) -> Result<()> {
|
||||
self.listener.reregister(registry, token, interests)
|
||||
}
|
||||
|
||||
fn deregister(&mut self, registry: &Registry) -> Result<()> {
|
||||
self.listener.deregister(registry)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl Drop for FrontendListener {
|
||||
fn drop(&mut self) {
|
||||
@@ -165,72 +254,19 @@ impl Drop for FrontendListener {
|
||||
}
|
||||
}
|
||||
|
||||
enum ReceiveState {
|
||||
Len, Data,
|
||||
#[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])?)
|
||||
}
|
||||
|
||||
pub struct FrontendConnection {
|
||||
#[cfg(unix)]
|
||||
stream: UnixStream,
|
||||
#[cfg(windows)]
|
||||
stream: TcpStream,
|
||||
state: ReceiveState,
|
||||
len: usize,
|
||||
len_buf: [u8; std::mem::size_of::<usize>()],
|
||||
recieve_buf: [u8; 256], // FIXME
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl FrontendConnection {
|
||||
#[cfg(unix)]
|
||||
pub fn new(stream: UnixStream) -> Self {
|
||||
Self {
|
||||
stream,
|
||||
state: ReceiveState::Len,
|
||||
len: 0,
|
||||
len_buf: [0u8; std::mem::size_of::<usize>()],
|
||||
recieve_buf: [0u8; 256],
|
||||
pos: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn new(stream: TcpStream) -> Self {
|
||||
Self {
|
||||
stream,
|
||||
state: ReceiveState::Len,
|
||||
len: 0,
|
||||
len_buf: [0u8; std::mem::size_of::<usize>()],
|
||||
recieve_buf: [0u8; 256],
|
||||
pos: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_event(&mut self) -> Result<Option<FrontendEvent>> {
|
||||
match self.state {
|
||||
ReceiveState::Len => {
|
||||
// we receive sizeof(usize) Bytes
|
||||
let n = self.stream.read(&mut self.len_buf)?;
|
||||
self.pos += n;
|
||||
if self.pos == self.len_buf.len() {
|
||||
self.state = ReceiveState::Data;
|
||||
self.len = usize::from_ne_bytes(self.len_buf);
|
||||
self.pos = 0;
|
||||
}
|
||||
Ok(None)
|
||||
},
|
||||
ReceiveState::Data => {
|
||||
// read at most as many bytes as the length of the next event
|
||||
let n = self.stream.read(&mut self.recieve_buf[..self.len])?;
|
||||
self.pos += n;
|
||||
if n == self.len {
|
||||
self.state = ReceiveState::Len;
|
||||
self.pos = 0;
|
||||
Ok(Some(serde_json::from_slice(&self.recieve_buf[..self.len])?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[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,28 +1,17 @@
|
||||
use anyhow::{anyhow, Result, Context};
|
||||
use std::{thread::{self, JoinHandle}, io::{Write, Read, ErrorKind}, str::SplitWhitespace};
|
||||
#[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, config::DEFAULT_PORT};
|
||||
|
||||
use super::{FrontendEvent, FrontendNotify};
|
||||
|
||||
pub fn start() -> Result<(JoinHandle<()>, JoinHandle<()>)> {
|
||||
#[cfg(unix)]
|
||||
let socket_path = Path::new(env::var("XDG_RUNTIME_DIR")?.as_str()).join("lan-mouse-socket.sock");
|
||||
|
||||
#[cfg(unix)]
|
||||
let Ok(mut tx) = UnixStream::connect(&socket_path) else {
|
||||
return Err(anyhow!("Could not connect to lan-mouse-socket"));
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
let Ok(mut tx) = TcpStream::connect("127.0.0.1:5252".parse::<SocketAddrV4>().unwrap()) else {
|
||||
pub fn run() -> Result<()> {
|
||||
let Ok(mut tx) = super::wait_for_service() else {
|
||||
return Err(anyhow!("Could not connect to lan-mouse-socket"));
|
||||
};
|
||||
|
||||
@@ -31,42 +20,44 @@ pub fn start() -> Result<(JoinHandle<()>, JoinHandle<()>)> {
|
||||
let reader = thread::Builder::new()
|
||||
.name("cli-frontend".to_string())
|
||||
.spawn(move || {
|
||||
// all further prompts
|
||||
prompt();
|
||||
loop {
|
||||
let mut buf = String::new();
|
||||
match std::io::stdin().read_line(&mut buf) {
|
||||
Ok(0) => break,
|
||||
Ok(len) => {
|
||||
if let Some(events) = parse_cmd(buf, len) {
|
||||
for event in events.iter() {
|
||||
let json = serde_json::to_string(&event).unwrap();
|
||||
let bytes = json.as_bytes();
|
||||
let len = bytes.len().to_ne_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() {
|
||||
break;
|
||||
// all further prompts
|
||||
prompt();
|
||||
loop {
|
||||
let mut buf = String::new();
|
||||
match std::io::stdin().read_line(&mut buf) {
|
||||
Ok(0) => return,
|
||||
Ok(len) => {
|
||||
if let Some(events) = parse_cmd(buf, len) {
|
||||
for event in events.iter() {
|
||||
let json = serde_json::to_string(&event).unwrap();
|
||||
let bytes = json.as_bytes();
|
||||
let len = bytes.len().to_be_bytes();
|
||||
if let Err(e) = tx.write(&len) {
|
||||
log::error!("error sending message: {e}");
|
||||
};
|
||||
if let Err(e) = tx.write(bytes) {
|
||||
log::error!("error sending message: {e}");
|
||||
};
|
||||
if *event == FrontendEvent::Shutdown() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// prompt is printed after the server response is received
|
||||
} else {
|
||||
prompt();
|
||||
}
|
||||
// prompt is printed after the server response is received
|
||||
} else {
|
||||
prompt();
|
||||
}
|
||||
Err(e) => {
|
||||
if e.kind() != ErrorKind::UnexpectedEof {
|
||||
log::error!("error reading from stdin: {e}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("error reading from stdin: {e}");
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})?;
|
||||
})?;
|
||||
|
||||
let writer = thread::Builder::new()
|
||||
let _ = thread::Builder::new()
|
||||
.name("cli-frontend-notify".to_string())
|
||||
.spawn(move || {
|
||||
loop {
|
||||
@@ -77,7 +68,7 @@ pub fn start() -> Result<(JoinHandle<()>, JoinHandle<()>)> {
|
||||
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break,
|
||||
Err(e) => break log::error!("{e}"),
|
||||
};
|
||||
let len = usize::from_ne_bytes(len);
|
||||
let len = usize::from_be_bytes(len);
|
||||
|
||||
// read payload
|
||||
let mut buf: Vec<u8> = vec![0u8; len];
|
||||
@@ -92,35 +83,69 @@ pub fn start() -> Result<(JoinHandle<()>, JoinHandle<()>)> {
|
||||
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::NotifyClientActivate(handle, active) => {
|
||||
if active {
|
||||
log::info!("client {handle} activated");
|
||||
} else {
|
||||
log::info!("client {handle} deactivated");
|
||||
}
|
||||
}
|
||||
FrontendNotify::NotifyClientCreate(client) => {
|
||||
let handle = client.handle;
|
||||
let port = client.port;
|
||||
let pos = client.pos;
|
||||
let hostname = client.hostname.as_deref().unwrap_or("");
|
||||
log::info!("new client ({handle}): {hostname}:{port} - {pos}");
|
||||
}
|
||||
FrontendNotify::NotifyClientUpdate(client) => {
|
||||
let handle = client.handle;
|
||||
let port = client.port;
|
||||
let pos = client.pos;
|
||||
let hostname = client.hostname.as_deref().unwrap_or("");
|
||||
log::info!("client ({handle}) updated: {hostname}:{port} - {pos}");
|
||||
}
|
||||
FrontendNotify::NotifyClientDelete(client) => {
|
||||
log::info!("client ({client}) deleted.");
|
||||
},
|
||||
}
|
||||
FrontendNotify::NotifyError(e) => {
|
||||
log::warn!("{e}");
|
||||
},
|
||||
}
|
||||
FrontendNotify::Enumerate(clients) => {
|
||||
for (client, active) in clients.into_iter() {
|
||||
log::info!("client ({}) [{}]: active: {}, associated addresses: [{}]",
|
||||
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(", ")
|
||||
client
|
||||
.ips
|
||||
.into_iter()
|
||||
.map(|a| a.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
);
|
||||
}
|
||||
}
|
||||
FrontendNotify::NotifyPortChange(port, msg) => match msg {
|
||||
Some(msg) => log::info!("could not change port: {msg}"),
|
||||
None => log::info!("port changed: {port}"),
|
||||
},
|
||||
}
|
||||
prompt();
|
||||
}
|
||||
})?;
|
||||
Ok((reader, writer))
|
||||
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 prompt() {
|
||||
@@ -130,7 +155,7 @@ fn prompt() {
|
||||
|
||||
fn parse_cmd(s: String, len: usize) -> Option<Vec<FrontendEvent>> {
|
||||
if len == 0 {
|
||||
return Some(vec![FrontendEvent::Shutdown()])
|
||||
return Some(vec![FrontendEvent::Shutdown()]);
|
||||
}
|
||||
let mut l = s.split_whitespace();
|
||||
let cmd = l.next()?;
|
||||
@@ -142,6 +167,7 @@ fn parse_cmd(s: String, len: usize) -> Option<Vec<FrontendEvent>> {
|
||||
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()]),
|
||||
@@ -150,22 +176,23 @@ fn parse_cmd(s: String, len: usize) -> Option<Vec<FrontendEvent>> {
|
||||
"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(vec![e, FrontendEvent::Enumerate()]),
|
||||
Some(Ok(e)) => Some(e),
|
||||
Some(Err(e)) => {
|
||||
log::warn!("{e}");
|
||||
None
|
||||
}
|
||||
_ => None
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_connect(mut l: SplitWhitespace) -> Result<FrontendEvent> {
|
||||
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)? {
|
||||
@@ -179,19 +206,37 @@ fn parse_connect(mut l: SplitWhitespace) -> Result<FrontendEvent> {
|
||||
} else {
|
||||
DEFAULT_PORT
|
||||
};
|
||||
Ok(FrontendEvent::AddClient(Some(host), port, pos))
|
||||
Ok(vec![
|
||||
FrontendEvent::AddClient(Some(host), port, pos),
|
||||
FrontendEvent::Enumerate(),
|
||||
])
|
||||
}
|
||||
|
||||
fn parse_disconnect(mut l: SplitWhitespace) -> Result<FrontendEvent> {
|
||||
fn parse_disconnect(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> {
|
||||
let client = l.next().context("usage: disconnect <client_id>")?.parse()?;
|
||||
Ok(FrontendEvent::DelClient(client))
|
||||
Ok(vec![
|
||||
FrontendEvent::DelClient(client),
|
||||
FrontendEvent::Enumerate(),
|
||||
])
|
||||
}
|
||||
|
||||
fn parse_activate(mut l: SplitWhitespace) -> Result<FrontendEvent> {
|
||||
fn parse_activate(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> {
|
||||
let client = l.next().context("usage: activate <client_id>")?.parse()?;
|
||||
Ok(FrontendEvent::ActivateClient(client, true))
|
||||
Ok(vec![
|
||||
FrontendEvent::ActivateClient(client, true),
|
||||
FrontendEvent::Enumerate(),
|
||||
])
|
||||
}
|
||||
fn parse_deactivate(mut l: SplitWhitespace) -> Result<FrontendEvent> {
|
||||
|
||||
fn parse_deactivate(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> {
|
||||
let client = l.next().context("usage: deactivate <client_id>")?.parse()?;
|
||||
Ok(FrontendEvent::ActivateClient(client, false))
|
||||
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)])
|
||||
}
|
||||
|
||||
@@ -1,69 +1,88 @@
|
||||
mod window;
|
||||
mod client_object;
|
||||
mod client_row;
|
||||
mod window;
|
||||
|
||||
use std::{io::{Result, Read, ErrorKind}, thread::{self, JoinHandle}, env, process, path::Path, os::unix::net::UnixStream, str};
|
||||
use std::{
|
||||
env,
|
||||
io::{ErrorKind, Read},
|
||||
process, str,
|
||||
};
|
||||
|
||||
use crate::{frontend::gtk::window::Window, config::DEFAULT_PORT};
|
||||
use crate::frontend::gtk::window::Window;
|
||||
|
||||
use gtk::{prelude::*, IconTheme, gdk::Display, gio::{SimpleAction, SimpleActionGroup}, glib::{clone, MainContext, Priority}, CssProvider, subclass::prelude::ObjectSubclassIsExt};
|
||||
use adw::Application;
|
||||
use gtk::{
|
||||
gdk::Display, glib::clone, prelude::*, subclass::prelude::ObjectSubclassIsExt, CssProvider,
|
||||
IconTheme,
|
||||
};
|
||||
use gtk::{gio, glib, prelude::ApplicationExt};
|
||||
|
||||
use self::client_object::ClientObject;
|
||||
|
||||
use super::FrontendNotify;
|
||||
|
||||
pub fn start() -> Result<JoinHandle<glib::ExitCode>> {
|
||||
thread::Builder::new()
|
||||
.name("gtk-thread".into())
|
||||
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();
|
||||
|
||||
if ret == glib::ExitCode::FAILURE {
|
||||
log::error!("frontend exited with failure");
|
||||
} else {
|
||||
log::info!("frontend exited successfully");
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
fn gtk_main() -> glib::ExitCode {
|
||||
gio::resources_register_include!("lan-mouse.gresource")
|
||||
.expect("Failed to register resources.");
|
||||
gio::resources_register_include!("lan-mouse.gresource").expect("Failed to register resources.");
|
||||
|
||||
let app = Application::builder()
|
||||
.application_id("de.feschber.lan-mouse")
|
||||
.application_id("de.feschber.LanMouse")
|
||||
.build();
|
||||
|
||||
app.connect_startup(|_| load_icons());
|
||||
app.connect_startup(|_| load_css());
|
||||
app.connect_activate(build_ui);
|
||||
|
||||
app.run()
|
||||
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."),
|
||||
&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."));
|
||||
let display = &Display::default().expect("Could not connect to a display.");
|
||||
let icon_theme = IconTheme::for_display(display);
|
||||
icon_theme.add_resource_path("/de/feschber/LanMouse/icons");
|
||||
}
|
||||
|
||||
fn build_ui(app: &Application) {
|
||||
let xdg_runtime_dir = match env::var("XDG_RUNTIME_DIR") {
|
||||
Ok(v) => v,
|
||||
log::debug!("connecting to lan-mouse-socket");
|
||||
let mut rx = match super::wait_for_service() {
|
||||
Ok(stream) => stream,
|
||||
Err(e) => {
|
||||
log::error!("{e}");
|
||||
log::error!("could not connect to lan-mouse-socket: {e}");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
let socket_path = Path::new(xdg_runtime_dir.as_str())
|
||||
.join("lan-mouse-socket.sock");
|
||||
let Ok(mut rx) = UnixStream::connect(&socket_path) else {
|
||||
log::error!("Could not connect to lan-mouse-socket @ {socket_path:?}");
|
||||
process::exit(1);
|
||||
};
|
||||
let tx = match rx.try_clone() {
|
||||
Ok(sock) => sock,
|
||||
Err(e) => {
|
||||
@@ -71,8 +90,9 @@ fn build_ui(app: &Application) {
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let (sender, receiver) = MainContext::channel::<FrontendNotify>(Priority::default());
|
||||
log::debug!("connected to lan-mouse-socket");
|
||||
|
||||
let (sender, receiver) = async_channel::bounded(10);
|
||||
|
||||
gio::spawn_blocking(move || {
|
||||
match loop {
|
||||
@@ -83,7 +103,7 @@ fn build_ui(app: &Application) {
|
||||
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()),
|
||||
Err(e) => break Err(e),
|
||||
};
|
||||
let len = usize::from_ne_bytes(len);
|
||||
let len = usize::from_be_bytes(len);
|
||||
|
||||
// read payload
|
||||
let mut buf = vec![0u8; len];
|
||||
@@ -94,32 +114,34 @@ fn build_ui(app: &Application) {
|
||||
};
|
||||
|
||||
// parse json
|
||||
let json = str::from_utf8(&buf)
|
||||
.unwrap();
|
||||
let json = str::from_utf8(&buf).unwrap();
|
||||
match serde_json::from_str(json) {
|
||||
Ok(notify) => sender.send(notify).unwrap(),
|
||||
Ok(notify) => sender.send_blocking(notify).unwrap(),
|
||||
Err(e) => log::error!("{e}"),
|
||||
}
|
||||
} {
|
||||
Ok(()) => {},
|
||||
Ok(()) => {}
|
||||
Err(e) => log::error!("{e}"),
|
||||
}
|
||||
});
|
||||
|
||||
let window = Window::new(app);
|
||||
window.imp().stream.borrow_mut().replace(tx);
|
||||
receiver.attach(None, clone!(@weak window => @default-return glib::ControlFlow::Break,
|
||||
move |notify| {
|
||||
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::NotifyClientActivate(handle, active) => {
|
||||
window.activate_client(handle, active);
|
||||
}
|
||||
FrontendNotify::NotifyClientCreate(client) => {
|
||||
window.new_client(client, false);
|
||||
},
|
||||
FrontendNotify::NotifyClientUpdate(client, hostname, port, position) => {
|
||||
log::info!("client updated: {client}, {}:{port}, {position}", hostname.unwrap_or("".to_string()));
|
||||
FrontendNotify::NotifyClientUpdate(client) => {
|
||||
window.update_client(client);
|
||||
}
|
||||
FrontendNotify::NotifyError(e) => {
|
||||
// TODO
|
||||
log::error!("{e}");
|
||||
window.show_toast(e.as_str());
|
||||
},
|
||||
FrontendNotify::NotifyClientDelete(client) => {
|
||||
window.delete_client(client);
|
||||
@@ -127,61 +149,23 @@ fn build_ui(app: &Application) {
|
||||
FrontendNotify::Enumerate(clients) => {
|
||||
for (client, active) in clients {
|
||||
if window.client_idx(client.handle).is_some() {
|
||||
continue
|
||||
window.activate_client(client.handle, active);
|
||||
window.update_client(client);
|
||||
} else {
|
||||
window.new_client(client, active);
|
||||
}
|
||||
window.new_client(
|
||||
client.handle,
|
||||
client.hostname,
|
||||
client.addrs
|
||||
.iter()
|
||||
.next()
|
||||
.map(|s| s.port())
|
||||
.unwrap_or(DEFAULT_PORT),
|
||||
client.pos,
|
||||
active,
|
||||
);
|
||||
}
|
||||
},
|
||||
FrontendNotify::NotifyPortChange(port, msg) => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
glib::ControlFlow::Continue
|
||||
}
|
||||
));
|
||||
|
||||
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 as u32) 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();
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
mod imp;
|
||||
|
||||
use gtk::glib::{self, Object};
|
||||
use adw::subclass::prelude::*;
|
||||
use gtk::glib::{self, Object};
|
||||
|
||||
use crate::client::ClientHandle;
|
||||
use crate::client::{Client, ClientHandle};
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct ClientObject(ObjectSubclass<imp::ClientObject>);
|
||||
}
|
||||
|
||||
impl ClientObject {
|
||||
pub fn new(handle: ClientHandle, hostname: Option<String>, port: u32, position: String, active: bool) -> Self {
|
||||
pub fn new(client: Client, active: bool) -> Self {
|
||||
Object::builder()
|
||||
.property("handle", handle)
|
||||
.property("hostname", hostname)
|
||||
.property("port", port)
|
||||
.property("handle", client.handle)
|
||||
.property("hostname", client.hostname)
|
||||
.property("port", client.port as u32)
|
||||
.property("position", client.pos.to_string())
|
||||
.property("active", active)
|
||||
.property("position", position)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,7 @@ glib::wrapper! {
|
||||
|
||||
impl ClientRow {
|
||||
pub fn new(_client_object: &ClientObject) -> Self {
|
||||
Object::builder()
|
||||
.build()
|
||||
Object::builder().build()
|
||||
}
|
||||
|
||||
pub fn bind(&self, client_object: &ClientObject) {
|
||||
@@ -29,6 +28,12 @@ impl ClientRow {
|
||||
.sync_create()
|
||||
.build();
|
||||
|
||||
let switch_position_binding = client_object
|
||||
.bind_property("active", &self.imp().enable_switch.get(), "active")
|
||||
.bidirectional()
|
||||
.sync_create()
|
||||
.build();
|
||||
|
||||
let hostname_binding = client_object
|
||||
.bind_property("hostname", &self.imp().hostname.get(), "text")
|
||||
.transform_to(|_, v: Option<String>| {
|
||||
@@ -64,7 +69,7 @@ impl ClientRow {
|
||||
let port_binding = client_object
|
||||
.bind_property("port", &self.imp().port.get(), "text")
|
||||
.transform_from(|_, v: String| {
|
||||
if v == "" {
|
||||
if v.is_empty() {
|
||||
Some(DEFAULT_PORT as u32)
|
||||
} else {
|
||||
Some(v.parse::<u16>().unwrap_or(DEFAULT_PORT) as u32)
|
||||
@@ -86,30 +91,26 @@ impl ClientRow {
|
||||
.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_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),
|
||||
}
|
||||
.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(switch_position_binding);
|
||||
bindings.push(hostname_binding);
|
||||
bindings.push(title_binding);
|
||||
bindings.push(port_binding);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use std::cell::RefCell;
|
||||
|
||||
use glib::{Binding, subclass::InitializingObject};
|
||||
use adw::{prelude::*, ComboRow, ActionRow};
|
||||
use adw::subclass::prelude::*;
|
||||
use adw::{prelude::*, ActionRow, ComboRow};
|
||||
use glib::{subclass::InitializingObject, Binding};
|
||||
use gtk::glib::clone;
|
||||
use gtk::{glib, CompositeTemplate, Switch, Button};
|
||||
use gtk::glib::once_cell::sync::Lazy;
|
||||
use gtk::glib::subclass::Signal;
|
||||
use gtk::{glib, Button, CompositeTemplate, Switch};
|
||||
|
||||
#[derive(CompositeTemplate, Default)]
|
||||
#[template(resource = "/de/feschber/LanMouse/client_row.ui")]
|
||||
@@ -28,6 +30,8 @@ pub struct ClientRow {
|
||||
impl ObjectSubclass for ClientRow {
|
||||
// `NAME` needs to match `class` attribute of template
|
||||
const NAME: &'static str = "ClientRow";
|
||||
const ABSTRACT: bool = false;
|
||||
|
||||
type Type = super::ClientRow;
|
||||
type ParentType = adw::ExpanderRow;
|
||||
|
||||
@@ -44,30 +48,38 @@ impl ObjectSubclass for ClientRow {
|
||||
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);
|
||||
}));
|
||||
self.delete_button
|
||||
.connect_clicked(clone!(@weak self as row => move |button| {
|
||||
row.handle_client_delete(button);
|
||||
}));
|
||||
}
|
||||
|
||||
fn signals() -> &'static [glib::subclass::Signal] {
|
||||
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
|
||||
vec![
|
||||
Signal::builder("request-update")
|
||||
.param_types([bool::static_type()])
|
||||
.build(),
|
||||
Signal::builder("request-delete").build(),
|
||||
]
|
||||
});
|
||||
SIGNALS.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl ClientRow {
|
||||
#[template_callback]
|
||||
fn handle_client_set_state(&self, state: bool, switch: &Switch) -> bool {
|
||||
let idx = self.obj().index() as u32;
|
||||
switch.activate_action("win.request-client-update", Some(&idx.to_variant())).unwrap();
|
||||
switch.set_state(state);
|
||||
|
||||
fn handle_client_set_state(&self, state: bool, _switch: &Switch) -> bool {
|
||||
log::debug!("state change -> requesting update");
|
||||
self.obj().emit_by_name::<()>("request-update", &[&state]);
|
||||
true // dont run default handler
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn handle_client_delete(&self, button: &Button) {
|
||||
log::debug!("delete button pressed");
|
||||
let idx = self.obj().index() as u32;
|
||||
button
|
||||
.activate_action("win.request-client-delete", Some(&idx.to_variant()))
|
||||
.unwrap();
|
||||
fn handle_client_delete(&self, _button: &Button) {
|
||||
log::debug!("delete button pressed -> requesting delete");
|
||||
self.obj().emit_by_name::<()>("request-delete", &[]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,18 @@ use std::io::Write;
|
||||
|
||||
use adw::prelude::*;
|
||||
use adw::subclass::prelude::*;
|
||||
use gtk::{glib, gio, NoSelection};
|
||||
use glib::{clone, Object};
|
||||
use gtk::{
|
||||
gio,
|
||||
glib::{self, closure_local},
|
||||
NoSelection,
|
||||
};
|
||||
|
||||
use crate::{frontend::{gtk::client_object::ClientObject, FrontendEvent}, client::{Position, ClientHandle}, config::DEFAULT_PORT};
|
||||
use crate::{
|
||||
client::{Client, ClientHandle, Position},
|
||||
config::DEFAULT_PORT,
|
||||
frontend::{gtk::client_object::ClientObject, FrontendEvent},
|
||||
};
|
||||
|
||||
use super::client_row::ClientRow;
|
||||
|
||||
@@ -41,6 +49,18 @@ impl Window {
|
||||
clone!(@weak self as window => @default-panic, move |obj| {
|
||||
let client_object = obj.downcast_ref().expect("Expected object of type `ClientObject`.");
|
||||
let row = window.create_client_row(client_object);
|
||||
row.connect_closure("request-update", false, closure_local!(@strong window => move |row: ClientRow, active: bool| {
|
||||
let index = row.index() as u32;
|
||||
let Some(client) = window.clients().item(index) else {
|
||||
return;
|
||||
};
|
||||
let client = client.downcast_ref::<ClientObject>().unwrap();
|
||||
window.request_client_update(client, active);
|
||||
}));
|
||||
row.connect_closure("request-delete", false, closure_local!(@strong window => move |row: ClientRow| {
|
||||
let index = row.index() as u32;
|
||||
window.request_client_delete(index);
|
||||
}));
|
||||
row.upcast()
|
||||
})
|
||||
);
|
||||
@@ -58,7 +78,7 @@ impl Window {
|
||||
}
|
||||
|
||||
fn setup_icon(&self) {
|
||||
self.set_icon_name(Some("mouse-icon"));
|
||||
self.set_icon_name(Some("de.feschber.LanMouse"));
|
||||
}
|
||||
|
||||
fn create_client_row(&self, client_object: &ClientObject) -> ClientRow {
|
||||
@@ -67,23 +87,20 @@ impl Window {
|
||||
row
|
||||
}
|
||||
|
||||
pub fn new_client(&self, handle: ClientHandle, hostname: Option<String>, port: u16, position: Position, active: bool) {
|
||||
let client = ClientObject::new(handle, hostname, port as u32, position.to_string(), active);
|
||||
pub fn new_client(&self, client: Client, active: bool) {
|
||||
let client = ClientObject::new(client, active);
|
||||
self.clients().append(&client);
|
||||
self.set_placeholder_visible(false);
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
.map(|p| p as usize)
|
||||
self.clients().iter::<ClientObject>().position(|c| {
|
||||
if let Ok(c) = c {
|
||||
c.handle() == handle
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_client(&self, handle: ClientHandle) {
|
||||
@@ -98,32 +115,78 @@ impl Window {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_client(&self, client: Client) {
|
||||
let Some(idx) = self.client_idx(client.handle) else {
|
||||
log::warn!("could not find client with handle {}", client.handle);
|
||||
return;
|
||||
};
|
||||
let client_object = self.clients().item(idx as u32).unwrap();
|
||||
let client_object: &ClientObject = client_object.downcast_ref().unwrap();
|
||||
let data = client_object.get_data();
|
||||
|
||||
/* only change if it actually has changed, otherwise
|
||||
* the update signal is triggered */
|
||||
if data.hostname != client.hostname {
|
||||
client_object.set_hostname(client.hostname.unwrap_or("".into()));
|
||||
}
|
||||
if data.port != client.port as u32 {
|
||||
client_object.set_port(client.port as u32);
|
||||
}
|
||||
if data.position != client.pos.to_string() {
|
||||
client_object.set_position(client.pos.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn activate_client(&self, handle: ClientHandle, active: bool) {
|
||||
let Some(idx) = self.client_idx(handle) else {
|
||||
log::warn!("could not find client with handle {handle}");
|
||||
return;
|
||||
};
|
||||
let client_object = self.clients().item(idx as u32).unwrap();
|
||||
let client_object: &ClientObject = client_object.downcast_ref().unwrap();
|
||||
let data = client_object.get_data();
|
||||
if data.active != active {
|
||||
client_object.set_active(active);
|
||||
log::debug!("set active to {active}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_client_create(&self) {
|
||||
let event = FrontendEvent::AddClient(None, DEFAULT_PORT, Position::default());
|
||||
self.imp().set_port(DEFAULT_PORT);
|
||||
self.request(event);
|
||||
}
|
||||
|
||||
pub fn request_client_update(&self, client: &ClientObject) {
|
||||
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, active: bool) {
|
||||
let data = client.get_data();
|
||||
let position = match data.position.as_str() {
|
||||
"left" => Position::Left,
|
||||
"right" => Position::Right,
|
||||
"top" => Position::Top,
|
||||
"bottom" => Position::Bottom,
|
||||
let position = match Position::try_from(data.position.as_str()) {
|
||||
Ok(pos) => pos,
|
||||
_ => {
|
||||
log::error!("invalid position: {}", data.position);
|
||||
return
|
||||
return;
|
||||
}
|
||||
};
|
||||
let hostname = data.hostname;
|
||||
let port = data.port as u16;
|
||||
|
||||
let event = FrontendEvent::UpdateClient(client.handle(), hostname, port, position);
|
||||
log::debug!("requesting update: {event:?}");
|
||||
self.request(event);
|
||||
|
||||
let event = FrontendEvent::ActivateClient(client.handle(), !client.active());
|
||||
let event = FrontendEvent::ActivateClient(client.handle(), active);
|
||||
log::debug!("requesting activate: {event:?}");
|
||||
self.request(event);
|
||||
}
|
||||
|
||||
|
||||
pub fn request_client_delete(&self, idx: u32) {
|
||||
if let Some(obj) = self.clients().item(idx) {
|
||||
let client_object: &ClientObject = obj
|
||||
@@ -135,13 +198,13 @@ impl Window {
|
||||
}
|
||||
}
|
||||
|
||||
fn request(&self, event: FrontendEvent) {
|
||||
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_ne_bytes();
|
||||
let len = bytes.len().to_be_bytes();
|
||||
if let Err(e) = stream.write(&len) {
|
||||
log::error!("error sending message: {e}");
|
||||
};
|
||||
@@ -150,11 +213,9 @@ impl Window {
|
||||
};
|
||||
}
|
||||
|
||||
fn setup_callbacks(&self) {
|
||||
self.imp()
|
||||
.add_client_button
|
||||
.connect_clicked(clone!(@weak self as window => move |_| {
|
||||
window.request_client_create();
|
||||
}));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,48 @@
|
||||
use std::{cell::{Cell, RefCell}, os::unix::net::UnixStream};
|
||||
use std::cell::{Cell, RefCell};
|
||||
|
||||
#[cfg(windows)]
|
||||
use std::net::TcpStream;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
use glib::subclass::InitializingObject;
|
||||
use adw::{prelude::*, ActionRow};
|
||||
use adw::subclass::prelude::*;
|
||||
use gtk::{glib, Button, CompositeTemplate, ListBox, gio};
|
||||
use adw::{prelude::*, 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 {
|
||||
pub number: Cell<i32>,
|
||||
#[template_child]
|
||||
pub add_client_button: TemplateChild<Button>,
|
||||
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";
|
||||
const ABSTRACT: bool = false;
|
||||
|
||||
type Type = super::Window;
|
||||
type ParentType = gtk::ApplicationWindow;
|
||||
type ParentType = adw::ApplicationWindow;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.bind_template();
|
||||
@@ -39,24 +57,53 @@ impl ObjectSubclass for Window {
|
||||
#[gtk::template_callbacks]
|
||||
impl Window {
|
||||
#[template_callback]
|
||||
fn handle_button_clicked(&self, button: &Button) {
|
||||
let number_increased = self.number.get() + 1;
|
||||
self.number.set(number_increased);
|
||||
button.set_label(&number_increased.to_string())
|
||||
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();
|
||||
obj.setup_callbacks();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
86
src/main.rs
86
src/main.rs
@@ -1,17 +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::{Cli, Gtk}}, event::server::Server,
|
||||
frontend::{FrontendListener, cli},
|
||||
};
|
||||
use lan_mouse::{config::Config, frontend, server::Server};
|
||||
|
||||
#[cfg(all(unix, feature = "gtk"))]
|
||||
use lan_mouse::frontend::gtk;
|
||||
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);
|
||||
@@ -22,37 +17,60 @@ 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:?}");
|
||||
log::info!("release bind: {:?}", config.release_bind);
|
||||
|
||||
// start producing and consuming events
|
||||
let producer = producer::create()?;
|
||||
let consumer = consumer::create()?;
|
||||
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)?;
|
||||
#[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()?;
|
||||
}
|
||||
service.kill()?;
|
||||
}
|
||||
|
||||
// create frontend communication adapter
|
||||
let frontend_adapter = FrontendListener::new()?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
// start sending and receiving events
|
||||
let mut event_server = Server::new(config.port, producer, consumer, frontend_adapter)?;
|
||||
fn run_service(config: &Config) -> Result<()> {
|
||||
// create single threaded tokio runtime
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
.enable_time()
|
||||
.build()?;
|
||||
|
||||
// any threads need to be started after event_server sets up signal handling
|
||||
match config.frontend {
|
||||
#[cfg(all(unix, feature = "gtk"))]
|
||||
Gtk => { gtk::start()?; }
|
||||
#[cfg(any(not(feature = "gtk"), not(unix)))]
|
||||
Gtk => panic!("gtk frontend requested but feature not enabled!"),
|
||||
Cli => { cli::start()?; }
|
||||
};
|
||||
// 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");
|
||||
|
||||
// add clients from config
|
||||
config.get_clients().into_iter().for_each(|(c, h, port, p)| {
|
||||
event_server.add_client(h, c, port, p);
|
||||
});
|
||||
|
||||
log::info!("Press Ctrl+Alt+Shift+Super to release the mouse");
|
||||
// run event loop
|
||||
event_server.run()?;
|
||||
let server = Server::new(config);
|
||||
server.run().await?;
|
||||
|
||||
log::debug!("service exiting");
|
||||
anyhow::Ok(())
|
||||
}))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
106
src/producer.rs
106
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().await {
|
||||
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)>;
|
||||
fn notify(&mut self, event: ClientEvent) -> io::Result<()>;
|
||||
|
||||
/// release mouse
|
||||
fn release(&mut self);
|
||||
fn release(&mut self) -> io::Result<()>;
|
||||
}
|
||||
|
||||
700
src/scancode.rs
Normal file
700
src/scancode.rs
Normal file
@@ -0,0 +1,700 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/*
|
||||
* 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(Serialize, Deserialize, Debug, Clone, Copy, Eq, Hash, PartialEq)]
|
||||
#[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(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
206
src/server.rs
Normal file
206
src/server.rs
Normal file
@@ -0,0 +1,206 @@
|
||||
use log;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
rc::Rc,
|
||||
};
|
||||
use tokio::signal;
|
||||
|
||||
use crate::{
|
||||
client::{ClientHandle, ClientManager},
|
||||
config::Config,
|
||||
dns,
|
||||
frontend::{FrontendEvent, FrontendListener},
|
||||
server::producer_task::ProducerEvent,
|
||||
};
|
||||
use crate::{consumer, producer};
|
||||
|
||||
use self::{consumer_task::ConsumerEvent, resolver_task::DnsRequest};
|
||||
|
||||
mod consumer_task;
|
||||
mod frontend_task;
|
||||
mod network_task;
|
||||
mod ping_task;
|
||||
mod producer_task;
|
||||
mod resolver_task;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum State {
|
||||
/// Currently sending events to another device
|
||||
Sending,
|
||||
/// Currently receiving events from other devices
|
||||
Receiving,
|
||||
/// Entered the deadzone of another device but waiting
|
||||
/// for acknowledgement (Leave event) from the device
|
||||
AwaitingLeave,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Server {
|
||||
active_client: Rc<Cell<Option<ClientHandle>>>,
|
||||
client_manager: Rc<RefCell<ClientManager>>,
|
||||
port: Rc<Cell<u16>>,
|
||||
state: Rc<Cell<State>>,
|
||||
release_bind: Vec<crate::scancode::Linux>,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub fn new(config: &Config) -> Self {
|
||||
let active_client = Rc::new(Cell::new(None));
|
||||
let client_manager = Rc::new(RefCell::new(ClientManager::new()));
|
||||
let state = Rc::new(Cell::new(State::Receiving));
|
||||
let port = Rc::new(Cell::new(config.port));
|
||||
for config_client in config.get_clients() {
|
||||
client_manager.borrow_mut().add_client(
|
||||
config_client.hostname,
|
||||
config_client.ips,
|
||||
config_client.port,
|
||||
config_client.pos,
|
||||
config_client.active,
|
||||
);
|
||||
}
|
||||
let release_bind = config.release_bind.clone();
|
||||
Self {
|
||||
active_client,
|
||||
client_manager,
|
||||
port,
|
||||
state,
|
||||
release_bind,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(&self) -> anyhow::Result<()> {
|
||||
// create frontend communication adapter
|
||||
let frontend = match FrontendListener::new().await {
|
||||
Some(f) => f?,
|
||||
None => {
|
||||
// none means some other instance is already running
|
||||
log::info!("service already running, exiting");
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
};
|
||||
let (consumer, producer) = tokio::join!(consumer::create(), producer::create());
|
||||
|
||||
let (timer_tx, timer_rx) = tokio::sync::mpsc::channel(1);
|
||||
let (frontend_notify_tx, frontend_notify_rx) = tokio::sync::mpsc::channel(1);
|
||||
|
||||
// udp task
|
||||
let (mut udp_task, sender_tx, receiver_rx, port_tx) =
|
||||
network_task::new(self.clone(), frontend_notify_tx).await?;
|
||||
|
||||
// event producer
|
||||
let (mut producer_task, producer_channel) = producer_task::new(
|
||||
producer,
|
||||
self.clone(),
|
||||
sender_tx.clone(),
|
||||
timer_tx.clone(),
|
||||
self.release_bind.clone(),
|
||||
);
|
||||
|
||||
// event consumer
|
||||
let (mut consumer_task, consumer_channel) = consumer_task::new(
|
||||
consumer,
|
||||
self.clone(),
|
||||
receiver_rx,
|
||||
sender_tx.clone(),
|
||||
producer_channel.clone(),
|
||||
timer_tx,
|
||||
);
|
||||
|
||||
// create dns resolver
|
||||
let resolver = dns::DnsResolver::new().await?;
|
||||
let (mut resolver_task, resolve_tx) = resolver_task::new(resolver, self.clone());
|
||||
|
||||
// frontend listener
|
||||
let (mut frontend_task, frontend_tx) = frontend_task::new(
|
||||
frontend,
|
||||
frontend_notify_rx,
|
||||
self.clone(),
|
||||
producer_channel.clone(),
|
||||
consumer_channel.clone(),
|
||||
resolve_tx.clone(),
|
||||
port_tx,
|
||||
);
|
||||
|
||||
// task that pings clients to see if they are responding
|
||||
let mut ping_task = ping_task::new(
|
||||
self.clone(),
|
||||
sender_tx.clone(),
|
||||
consumer_channel.clone(),
|
||||
producer_channel.clone(),
|
||||
timer_rx,
|
||||
);
|
||||
|
||||
let active = self
|
||||
.client_manager
|
||||
.borrow()
|
||||
.get_client_states()
|
||||
.filter_map(|s| {
|
||||
if s.active {
|
||||
Some((s.client.handle, s.client.hostname.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for (handle, hostname) in active {
|
||||
frontend_tx
|
||||
.send(FrontendEvent::ActivateClient(handle, true))
|
||||
.await?;
|
||||
if let Some(hostname) = hostname {
|
||||
let _ = resolve_tx.send(DnsRequest { hostname, handle }).await;
|
||||
}
|
||||
}
|
||||
log::info!("running service");
|
||||
|
||||
tokio::select! {
|
||||
_ = signal::ctrl_c() => {
|
||||
log::info!("terminating service");
|
||||
}
|
||||
e = &mut producer_task => {
|
||||
if let Ok(Err(e)) = e {
|
||||
log::error!("error in event producer: {e}");
|
||||
}
|
||||
}
|
||||
e = &mut consumer_task => {
|
||||
if let Ok(Err(e)) = e {
|
||||
log::error!("error in event consumer: {e}");
|
||||
}
|
||||
}
|
||||
e = &mut frontend_task => {
|
||||
if let Ok(Err(e)) = e {
|
||||
log::error!("error in frontend listener: {e}");
|
||||
}
|
||||
}
|
||||
_ = &mut resolver_task => { }
|
||||
_ = &mut udp_task => { }
|
||||
_ = &mut ping_task => { }
|
||||
}
|
||||
|
||||
let _ = consumer_channel.send(ConsumerEvent::Terminate).await;
|
||||
let _ = producer_channel.send(ProducerEvent::Terminate).await;
|
||||
let _ = frontend_tx.send(FrontendEvent::Shutdown()).await;
|
||||
|
||||
if !producer_task.is_finished() {
|
||||
if let Err(e) = producer_task.await {
|
||||
log::error!("error in event producer: {e}");
|
||||
}
|
||||
}
|
||||
if !consumer_task.is_finished() {
|
||||
if let Err(e) = consumer_task.await {
|
||||
log::error!("error in event consumer: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
if !frontend_task.is_finished() {
|
||||
if let Err(e) = frontend_task.await {
|
||||
log::error!("error in frontend listener: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
resolver_task.abort();
|
||||
udp_task.abort();
|
||||
ping_task.abort();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
240
src/server/consumer_task.rs
Normal file
240
src/server/consumer_task.rs
Normal file
@@ -0,0 +1,240 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use tokio::{
|
||||
sync::mpsc::{Receiver, Sender},
|
||||
task::JoinHandle,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
client::{ClientEvent, ClientHandle},
|
||||
consumer::EventConsumer,
|
||||
event::{Event, KeyboardEvent},
|
||||
scancode,
|
||||
server::State,
|
||||
};
|
||||
|
||||
use super::{ProducerEvent, Server};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ConsumerEvent {
|
||||
/// consumer is notified of a change in client states
|
||||
ClientEvent(ClientEvent),
|
||||
/// consumer must release keys for client
|
||||
ReleaseKeys(ClientHandle),
|
||||
/// termination signal
|
||||
Terminate,
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
mut consumer: Box<dyn EventConsumer>,
|
||||
server: Server,
|
||||
mut udp_rx: Receiver<Result<(Event, SocketAddr)>>,
|
||||
sender_tx: Sender<(Event, SocketAddr)>,
|
||||
producer_tx: Sender<ProducerEvent>,
|
||||
timer_tx: Sender<()>,
|
||||
) -> (JoinHandle<Result<()>>, Sender<ConsumerEvent>) {
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
|
||||
let consumer_task = tokio::task::spawn_local(async move {
|
||||
let mut last_ignored = None;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
udp_event = udp_rx.recv() => {
|
||||
let udp_event = udp_event.ok_or(anyhow!("receiver closed"))??;
|
||||
handle_udp_rx(&server, &producer_tx, &mut consumer, &sender_tx, &mut last_ignored, udp_event, &timer_tx).await;
|
||||
}
|
||||
consumer_event = rx.recv() => {
|
||||
match consumer_event {
|
||||
Some(e) => match e {
|
||||
ConsumerEvent::ClientEvent(e) => consumer.notify(e).await,
|
||||
ConsumerEvent::ReleaseKeys(c) => release_keys(&server, &mut consumer, c).await,
|
||||
ConsumerEvent::Terminate => break,
|
||||
},
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
res = consumer.dispatch() => {
|
||||
res?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// release potentially still pressed keys
|
||||
let clients = server
|
||||
.client_manager
|
||||
.borrow()
|
||||
.get_client_states()
|
||||
.map(|s| s.client.handle)
|
||||
.collect::<Vec<_>>();
|
||||
for client in clients {
|
||||
release_keys(&server, &mut consumer, client).await;
|
||||
}
|
||||
|
||||
// destroy consumer
|
||||
consumer.destroy().await;
|
||||
anyhow::Ok(())
|
||||
});
|
||||
(consumer_task, tx)
|
||||
}
|
||||
|
||||
async fn handle_udp_rx(
|
||||
server: &Server,
|
||||
producer_notify_tx: &Sender<ProducerEvent>,
|
||||
consumer: &mut Box<dyn EventConsumer>,
|
||||
sender_tx: &Sender<(Event, SocketAddr)>,
|
||||
last_ignored: &mut Option<SocketAddr>,
|
||||
event: (Event, SocketAddr),
|
||||
timer_tx: &Sender<()>,
|
||||
) {
|
||||
let (event, addr) = event;
|
||||
|
||||
// get handle for addr
|
||||
let handle = match server.client_manager.borrow().get_client(addr) {
|
||||
Some(a) => a,
|
||||
None => {
|
||||
if last_ignored.is_none() || last_ignored.is_some() && last_ignored.unwrap() != addr {
|
||||
log::warn!("ignoring events from client {addr}");
|
||||
last_ignored.replace(addr);
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// next event can be logged as ignored again
|
||||
last_ignored.take();
|
||||
|
||||
log::trace!("{:20} <-<-<-<------ {addr} ({handle})", event.to_string());
|
||||
{
|
||||
let mut client_manager = server.client_manager.borrow_mut();
|
||||
let client_state = match client_manager.get_mut(handle) {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
log::error!("unknown handle");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// reset ttl for client and
|
||||
client_state.alive = true;
|
||||
// set addr as new default for this client
|
||||
client_state.active_addr = Some(addr);
|
||||
}
|
||||
|
||||
match (event, addr) {
|
||||
(Event::Pong(), _) => { /* ignore pong events */ }
|
||||
(Event::Ping(), addr) => {
|
||||
let _ = sender_tx.send((Event::Pong(), addr)).await;
|
||||
}
|
||||
(Event::Disconnect(), _) => {
|
||||
release_keys(server, consumer, handle).await;
|
||||
}
|
||||
(event, addr) => {
|
||||
// tell clients that we are ready to receive events
|
||||
if let Event::Enter() = event {
|
||||
let _ = sender_tx.send((Event::Leave(), addr)).await;
|
||||
}
|
||||
|
||||
match server.state.get() {
|
||||
State::Sending => {
|
||||
if let Event::Leave() = event {
|
||||
// ignore additional leave events that may
|
||||
// have been sent for redundancy
|
||||
} else {
|
||||
// upon receiving any event, we go back to receiving mode
|
||||
server.state.replace(State::Receiving);
|
||||
let _ = producer_notify_tx.send(ProducerEvent::Release).await;
|
||||
log::trace!("STATE ===> Receiving");
|
||||
}
|
||||
}
|
||||
State::Receiving => {
|
||||
let mut ignore_event = false;
|
||||
if let Event::Keyboard(KeyboardEvent::Key {
|
||||
time: _,
|
||||
key,
|
||||
state,
|
||||
}) = event
|
||||
{
|
||||
let mut client_manager = server.client_manager.borrow_mut();
|
||||
let client_state =
|
||||
if let Some(client_state) = client_manager.get_mut(handle) {
|
||||
client_state
|
||||
} else {
|
||||
log::error!("unknown handle");
|
||||
return;
|
||||
};
|
||||
if state == 0 {
|
||||
// ignore release event if key not pressed
|
||||
ignore_event = !client_state.pressed_keys.remove(&key);
|
||||
} else {
|
||||
// ignore press event if key not released
|
||||
ignore_event = !client_state.pressed_keys.insert(key);
|
||||
let _ = timer_tx.try_send(());
|
||||
}
|
||||
}
|
||||
// ignore double press / release events to
|
||||
// workaround buggy rdp backend.
|
||||
if !ignore_event {
|
||||
// consume event
|
||||
consumer.consume(event, handle).await;
|
||||
log::trace!("{event:?} => consumer");
|
||||
}
|
||||
}
|
||||
State::AwaitingLeave => {
|
||||
// we just entered the deadzone of a client, so
|
||||
// we need to ignore events that may still
|
||||
// be on the way until a leave event occurs
|
||||
// telling us the client registered the enter
|
||||
if let Event::Leave() = event {
|
||||
server.state.replace(State::Sending);
|
||||
log::trace!("STATE ===> Sending");
|
||||
}
|
||||
|
||||
// entering a client that is waiting for a leave
|
||||
// event should still be possible
|
||||
if let Event::Enter() = event {
|
||||
server.state.replace(State::Receiving);
|
||||
let _ = producer_notify_tx.send(ProducerEvent::Release).await;
|
||||
log::trace!("STATE ===> Receiving");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn release_keys(
|
||||
server: &Server,
|
||||
consumer: &mut Box<dyn EventConsumer>,
|
||||
client: ClientHandle,
|
||||
) {
|
||||
let keys = server
|
||||
.client_manager
|
||||
.borrow_mut()
|
||||
.get_mut(client)
|
||||
.iter_mut()
|
||||
.flat_map(|s| s.pressed_keys.drain())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for key in keys {
|
||||
let event = Event::Keyboard(KeyboardEvent::Key {
|
||||
time: 0,
|
||||
key,
|
||||
state: 0,
|
||||
});
|
||||
consumer.consume(event, client).await;
|
||||
if let Ok(key) = scancode::Linux::try_from(key) {
|
||||
log::warn!("releasing stuck key: {key:?}");
|
||||
}
|
||||
}
|
||||
|
||||
let modifiers_event = KeyboardEvent::Modifiers {
|
||||
mods_depressed: 0,
|
||||
mods_latched: 0,
|
||||
mods_locked: 0,
|
||||
group: 0,
|
||||
};
|
||||
consumer
|
||||
.consume(Event::Keyboard(modifiers_event), client)
|
||||
.await;
|
||||
}
|
||||
335
src/server/frontend_task.rs
Normal file
335
src/server/frontend_task.rs
Normal file
@@ -0,0 +1,335 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
io::ErrorKind,
|
||||
net::{IpAddr, SocketAddr},
|
||||
};
|
||||
#[cfg(unix)]
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
#[cfg(windows)]
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use tokio::{
|
||||
io::ReadHalf,
|
||||
sync::mpsc::{Receiver, Sender},
|
||||
task::JoinHandle,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
client::{ClientEvent, ClientHandle, Position},
|
||||
frontend::{self, FrontendEvent, FrontendListener, FrontendNotify},
|
||||
};
|
||||
|
||||
use super::{
|
||||
consumer_task::ConsumerEvent, producer_task::ProducerEvent, resolver_task::DnsRequest, Server,
|
||||
};
|
||||
|
||||
pub(crate) fn new(
|
||||
mut frontend: FrontendListener,
|
||||
mut notify_rx: Receiver<FrontendNotify>,
|
||||
server: Server,
|
||||
producer_notify: Sender<ProducerEvent>,
|
||||
consumer_notify: Sender<ConsumerEvent>,
|
||||
resolve_ch: Sender<DnsRequest>,
|
||||
port_tx: Sender<u16>,
|
||||
) -> (JoinHandle<Result<()>>, Sender<FrontendEvent>) {
|
||||
let (event_tx, mut event_rx) = tokio::sync::mpsc::channel(32);
|
||||
let event_tx_clone = event_tx.clone();
|
||||
let frontend_task = tokio::task::spawn_local(async move {
|
||||
loop {
|
||||
tokio::select! {
|
||||
stream = frontend.accept() => {
|
||||
let stream = match stream {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
log::warn!("error accepting frontend connection: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
handle_frontend_stream(&event_tx_clone, stream).await;
|
||||
}
|
||||
event = event_rx.recv() => {
|
||||
let frontend_event = event.ok_or(anyhow!("frontend channel closed"))?;
|
||||
if handle_frontend_event(&server, &producer_notify, &consumer_notify, &resolve_ch, &mut frontend, &port_tx, frontend_event).await {
|
||||
break;
|
||||
}
|
||||
}
|
||||
notify = notify_rx.recv() => {
|
||||
let notify = notify.ok_or(anyhow!("frontend notify closed"))?;
|
||||
let _ = frontend.notify_all(notify).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
anyhow::Ok(())
|
||||
});
|
||||
(frontend_task, event_tx)
|
||||
}
|
||||
|
||||
async fn handle_frontend_stream(
|
||||
frontend_tx: &Sender<FrontendEvent>,
|
||||
#[cfg(unix)] mut stream: ReadHalf<UnixStream>,
|
||||
#[cfg(windows)] mut stream: ReadHalf<TcpStream>,
|
||||
) {
|
||||
use std::io;
|
||||
|
||||
let tx = frontend_tx.clone();
|
||||
tokio::task::spawn_local(async move {
|
||||
let _ = tx.send(FrontendEvent::Enumerate()).await;
|
||||
loop {
|
||||
let event = frontend::read_event(&mut stream).await;
|
||||
match event {
|
||||
Ok(event) => {
|
||||
let _ = tx.send(event).await;
|
||||
}
|
||||
Err(e) => {
|
||||
if let Some(e) = e.downcast_ref::<io::Error>() {
|
||||
if e.kind() == ErrorKind::UnexpectedEof {
|
||||
return;
|
||||
}
|
||||
}
|
||||
log::error!("error reading frontend event: {e}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn handle_frontend_event(
|
||||
server: &Server,
|
||||
producer_tx: &Sender<ProducerEvent>,
|
||||
consumer_tx: &Sender<ConsumerEvent>,
|
||||
resolve_tx: &Sender<DnsRequest>,
|
||||
frontend: &mut FrontendListener,
|
||||
port_tx: &Sender<u16>,
|
||||
event: FrontendEvent,
|
||||
) -> bool {
|
||||
log::debug!("frontend: {event:?}");
|
||||
let response = match event {
|
||||
FrontendEvent::AddClient(hostname, port, pos) => {
|
||||
let handle = add_client(server, resolve_tx, hostname, HashSet::new(), port, pos).await;
|
||||
|
||||
let client = server
|
||||
.client_manager
|
||||
.borrow()
|
||||
.get(handle)
|
||||
.unwrap()
|
||||
.client
|
||||
.clone();
|
||||
Some(FrontendNotify::NotifyClientCreate(client))
|
||||
}
|
||||
FrontendEvent::ActivateClient(handle, active) => {
|
||||
activate_client(server, producer_tx, consumer_tx, handle, active).await;
|
||||
Some(FrontendNotify::NotifyClientActivate(handle, active))
|
||||
}
|
||||
FrontendEvent::ChangePort(port) => {
|
||||
let _ = port_tx.send(port).await;
|
||||
None
|
||||
}
|
||||
FrontendEvent::DelClient(handle) => {
|
||||
remove_client(server, producer_tx, consumer_tx, frontend, handle).await;
|
||||
Some(FrontendNotify::NotifyClientDelete(handle))
|
||||
}
|
||||
FrontendEvent::Enumerate() => {
|
||||
let clients = server
|
||||
.client_manager
|
||||
.borrow()
|
||||
.get_client_states()
|
||||
.map(|s| (s.client.clone(), s.active))
|
||||
.collect();
|
||||
Some(FrontendNotify::Enumerate(clients))
|
||||
}
|
||||
FrontendEvent::Shutdown() => {
|
||||
log::info!("terminating gracefully...");
|
||||
return true;
|
||||
}
|
||||
FrontendEvent::UpdateClient(handle, hostname, port, pos) => {
|
||||
update_client(
|
||||
server,
|
||||
producer_tx,
|
||||
consumer_tx,
|
||||
resolve_tx,
|
||||
(handle, hostname, port, pos),
|
||||
)
|
||||
.await;
|
||||
|
||||
let client = server
|
||||
.client_manager
|
||||
.borrow()
|
||||
.get(handle)
|
||||
.unwrap()
|
||||
.client
|
||||
.clone();
|
||||
Some(FrontendNotify::NotifyClientUpdate(client))
|
||||
}
|
||||
};
|
||||
let Some(response) = response else {
|
||||
return false;
|
||||
};
|
||||
if let Err(e) = frontend.notify_all(response).await {
|
||||
log::error!("error notifying frontend: {e}");
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn add_client(
|
||||
server: &Server,
|
||||
resolver_tx: &Sender<DnsRequest>,
|
||||
hostname: Option<String>,
|
||||
addr: HashSet<IpAddr>,
|
||||
port: u16,
|
||||
pos: Position,
|
||||
) -> ClientHandle {
|
||||
log::info!(
|
||||
"adding client [{}]{} @ {:?}",
|
||||
pos,
|
||||
hostname.as_deref().unwrap_or(""),
|
||||
&addr
|
||||
);
|
||||
let handle =
|
||||
server
|
||||
.client_manager
|
||||
.borrow_mut()
|
||||
.add_client(hostname.clone(), addr, port, pos, false);
|
||||
|
||||
log::debug!("add_client {handle}");
|
||||
|
||||
if let Some(hostname) = hostname {
|
||||
let _ = resolver_tx.send(DnsRequest { hostname, handle }).await;
|
||||
}
|
||||
|
||||
handle
|
||||
}
|
||||
|
||||
pub async fn activate_client(
|
||||
server: &Server,
|
||||
producer_notify_tx: &Sender<ProducerEvent>,
|
||||
consumer_notify_tx: &Sender<ConsumerEvent>,
|
||||
client: ClientHandle,
|
||||
active: bool,
|
||||
) {
|
||||
let (client, pos) = match server.client_manager.borrow_mut().get_mut(client) {
|
||||
Some(state) => {
|
||||
state.active = active;
|
||||
(state.client.handle, state.client.pos)
|
||||
}
|
||||
None => return,
|
||||
};
|
||||
if active {
|
||||
let _ = producer_notify_tx
|
||||
.send(ProducerEvent::ClientEvent(ClientEvent::Create(client, pos)))
|
||||
.await;
|
||||
let _ = consumer_notify_tx
|
||||
.send(ConsumerEvent::ClientEvent(ClientEvent::Create(client, pos)))
|
||||
.await;
|
||||
} else {
|
||||
let _ = producer_notify_tx
|
||||
.send(ProducerEvent::ClientEvent(ClientEvent::Destroy(client)))
|
||||
.await;
|
||||
let _ = consumer_notify_tx
|
||||
.send(ConsumerEvent::ClientEvent(ClientEvent::Destroy(client)))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn remove_client(
|
||||
server: &Server,
|
||||
producer_notify_tx: &Sender<ProducerEvent>,
|
||||
consumer_notify_tx: &Sender<ConsumerEvent>,
|
||||
frontend: &mut FrontendListener,
|
||||
client: ClientHandle,
|
||||
) -> Option<ClientHandle> {
|
||||
let Some((client, active)) = server
|
||||
.client_manager
|
||||
.borrow_mut()
|
||||
.remove_client(client)
|
||||
.map(|s| (s.client.handle, s.active))
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if active {
|
||||
let _ = producer_notify_tx
|
||||
.send(ProducerEvent::ClientEvent(ClientEvent::Destroy(client)))
|
||||
.await;
|
||||
let _ = consumer_notify_tx
|
||||
.send(ConsumerEvent::ClientEvent(ClientEvent::Destroy(client)))
|
||||
.await;
|
||||
}
|
||||
|
||||
let notify = FrontendNotify::NotifyClientDelete(client);
|
||||
log::debug!("{notify:?}");
|
||||
if let Err(e) = frontend.notify_all(notify).await {
|
||||
log::error!("error notifying frontend: {e}");
|
||||
}
|
||||
Some(client)
|
||||
}
|
||||
|
||||
async fn update_client(
|
||||
server: &Server,
|
||||
producer_notify_tx: &Sender<ProducerEvent>,
|
||||
consumer_notify_tx: &Sender<ConsumerEvent>,
|
||||
resolve_tx: &Sender<DnsRequest>,
|
||||
client_update: (ClientHandle, Option<String>, u16, Position),
|
||||
) {
|
||||
let (handle, hostname, port, pos) = client_update;
|
||||
let mut changed = false;
|
||||
let (hostname, handle, active) = {
|
||||
// retrieve state
|
||||
let mut client_manager = server.client_manager.borrow_mut();
|
||||
let Some(state) = client_manager.get_mut(handle) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// update pos
|
||||
if state.client.pos != pos {
|
||||
state.client.pos = pos;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// update port
|
||||
if state.client.port != port {
|
||||
state.client.port = port;
|
||||
state.active_addr = state.active_addr.map(|a| SocketAddr::new(a.ip(), port));
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// update hostname
|
||||
if state.client.hostname != hostname {
|
||||
state.client.ips = HashSet::new();
|
||||
state.active_addr = None;
|
||||
state.client.hostname = hostname;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
log::debug!("client updated: {:?}", state);
|
||||
(
|
||||
state.client.hostname.clone(),
|
||||
state.client.handle,
|
||||
state.active,
|
||||
)
|
||||
};
|
||||
|
||||
// update state in event consumer & producer
|
||||
if changed && active {
|
||||
// resolve dns
|
||||
if let Some(hostname) = hostname {
|
||||
let _ = resolve_tx.send(DnsRequest { hostname, handle }).await;
|
||||
}
|
||||
|
||||
// update state
|
||||
let _ = producer_notify_tx
|
||||
.send(ProducerEvent::ClientEvent(ClientEvent::Destroy(handle)))
|
||||
.await;
|
||||
let _ = consumer_notify_tx
|
||||
.send(ConsumerEvent::ClientEvent(ClientEvent::Destroy(handle)))
|
||||
.await;
|
||||
let _ = producer_notify_tx
|
||||
.send(ProducerEvent::ClientEvent(ClientEvent::Create(handle, pos)))
|
||||
.await;
|
||||
let _ = consumer_notify_tx
|
||||
.send(ConsumerEvent::ClientEvent(ClientEvent::Create(handle, pos)))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
90
src/server/network_task.rs
Normal file
90
src/server/network_task.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use anyhow::Result;
|
||||
use tokio::{
|
||||
net::UdpSocket,
|
||||
sync::mpsc::{Receiver, Sender},
|
||||
task::JoinHandle,
|
||||
};
|
||||
|
||||
use crate::{event::Event, frontend::FrontendNotify};
|
||||
|
||||
use super::Server;
|
||||
|
||||
pub async fn new(
|
||||
server: Server,
|
||||
frontend_notify_tx: Sender<FrontendNotify>,
|
||||
) -> Result<(
|
||||
JoinHandle<()>,
|
||||
Sender<(Event, SocketAddr)>,
|
||||
Receiver<Result<(Event, SocketAddr)>>,
|
||||
Sender<u16>,
|
||||
)> {
|
||||
// bind the udp socket
|
||||
let listen_addr = SocketAddr::new("0.0.0.0".parse().unwrap(), server.port.get());
|
||||
let mut socket = UdpSocket::bind(listen_addr).await?;
|
||||
let (receiver_tx, receiver_rx) = tokio::sync::mpsc::channel(32);
|
||||
let (sender_tx, mut sender_rx) = tokio::sync::mpsc::channel(32);
|
||||
let (port_tx, mut port_rx) = tokio::sync::mpsc::channel(32);
|
||||
|
||||
let udp_task = tokio::task::spawn_local(async move {
|
||||
loop {
|
||||
tokio::select! {
|
||||
event = receive_event(&socket) => {
|
||||
let _ = receiver_tx.send(event).await;
|
||||
}
|
||||
event = sender_rx.recv() => {
|
||||
let Some((event, addr)) = event else {
|
||||
break;
|
||||
};
|
||||
if let Err(e) = send_event(&socket, event, addr) {
|
||||
log::warn!("udp send failed: {e}");
|
||||
};
|
||||
}
|
||||
port = port_rx.recv() => {
|
||||
let Some(port) = port else {
|
||||
break;
|
||||
};
|
||||
|
||||
if socket.local_addr().unwrap().port() == port {
|
||||
continue;
|
||||
}
|
||||
|
||||
let listen_addr = SocketAddr::new("0.0.0.0".parse().unwrap(), port);
|
||||
match UdpSocket::bind(listen_addr).await {
|
||||
Ok(new_socket) => {
|
||||
socket = new_socket;
|
||||
server.port.replace(port);
|
||||
let _ = frontend_notify_tx.send(FrontendNotify::NotifyPortChange(port, None)).await;
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("could not change port: {e}");
|
||||
let port = socket.local_addr().unwrap().port();
|
||||
let _ = frontend_notify_tx.send(FrontendNotify::NotifyPortChange(
|
||||
port,
|
||||
Some(format!("could not change port: {e}")),
|
||||
)).await;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok((udp_task, sender_tx, receiver_rx, port_tx))
|
||||
}
|
||||
|
||||
async fn receive_event(socket: &UdpSocket) -> Result<(Event, SocketAddr)> {
|
||||
let mut buf = vec![0u8; 22];
|
||||
let (_amt, src) = socket.recv_from(&mut buf).await?;
|
||||
Ok((Event::try_from(buf)?, src))
|
||||
}
|
||||
|
||||
fn send_event(sock: &UdpSocket, e: Event, addr: SocketAddr) -> Result<usize> {
|
||||
log::trace!("{:20} ------>->->-> {addr}", e.to_string());
|
||||
let data: Vec<u8> = (&e).into();
|
||||
// When udp blocks, we dont want to block the event loop.
|
||||
// Dropping events is better than potentially crashing the event
|
||||
// producer.
|
||||
Ok(sock.try_send_to(&data, addr)?)
|
||||
}
|
||||
131
src/server/ping_task.rs
Normal file
131
src/server/ping_task.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
use std::{net::SocketAddr, time::Duration};
|
||||
|
||||
use tokio::{
|
||||
sync::mpsc::{Receiver, Sender},
|
||||
task::JoinHandle,
|
||||
};
|
||||
|
||||
use crate::{client::ClientHandle, event::Event};
|
||||
|
||||
use super::{consumer_task::ConsumerEvent, producer_task::ProducerEvent, Server, State};
|
||||
|
||||
const MAX_RESPONSE_TIME: Duration = Duration::from_millis(500);
|
||||
|
||||
pub fn new(
|
||||
server: Server,
|
||||
sender_ch: Sender<(Event, SocketAddr)>,
|
||||
consumer_notify: Sender<ConsumerEvent>,
|
||||
producer_notify: Sender<ProducerEvent>,
|
||||
mut timer_rx: Receiver<()>,
|
||||
) -> JoinHandle<()> {
|
||||
// timer task
|
||||
let ping_task = tokio::task::spawn_local(async move {
|
||||
loop {
|
||||
// wait for wake up signal
|
||||
let Some(_): Option<()> = timer_rx.recv().await else {
|
||||
break;
|
||||
};
|
||||
loop {
|
||||
let receiving = server.state.get() == State::Receiving;
|
||||
let (ping_clients, ping_addrs) = {
|
||||
let mut client_manager = server.client_manager.borrow_mut();
|
||||
|
||||
let ping_clients: Vec<ClientHandle> = if receiving {
|
||||
// if receiving we care about clients with pressed keys
|
||||
client_manager
|
||||
.get_client_states_mut()
|
||||
.filter(|s| !s.pressed_keys.is_empty())
|
||||
.map(|s| s.client.handle)
|
||||
.collect()
|
||||
} else {
|
||||
// if sending we care about the active client
|
||||
server.active_client.get().iter().cloned().collect()
|
||||
};
|
||||
|
||||
// get relevant socket addrs for clients
|
||||
let ping_addrs: Vec<SocketAddr> = {
|
||||
ping_clients
|
||||
.iter()
|
||||
.flat_map(|&c| client_manager.get(c))
|
||||
.flat_map(|state| {
|
||||
if state.alive && state.active_addr.is_some() {
|
||||
vec![state.active_addr.unwrap()]
|
||||
} else {
|
||||
state
|
||||
.client
|
||||
.ips
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|ip| SocketAddr::new(ip, state.client.port))
|
||||
.collect()
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
// reset alive
|
||||
for state in client_manager.get_client_states_mut() {
|
||||
state.alive = false;
|
||||
}
|
||||
|
||||
(ping_clients, ping_addrs)
|
||||
};
|
||||
|
||||
if receiving && ping_clients.is_empty() {
|
||||
// receiving and no client has pressed keys
|
||||
// -> no need to keep pinging
|
||||
break;
|
||||
}
|
||||
|
||||
// ping clients
|
||||
for addr in ping_addrs {
|
||||
if sender_ch.send((Event::Ping(), addr)).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// give clients time to resond
|
||||
if receiving {
|
||||
log::trace!("waiting {MAX_RESPONSE_TIME:?} for response from client with pressed keys ...");
|
||||
} else {
|
||||
log::trace!(
|
||||
"state: {:?} => waiting {MAX_RESPONSE_TIME:?} for client to respond ...",
|
||||
server.state.get()
|
||||
);
|
||||
}
|
||||
|
||||
tokio::time::sleep(MAX_RESPONSE_TIME).await;
|
||||
|
||||
// when anything is received from a client,
|
||||
// the alive flag gets set
|
||||
let unresponsive_clients: Vec<_> = {
|
||||
let client_manager = server.client_manager.borrow();
|
||||
ping_clients
|
||||
.iter()
|
||||
.filter_map(|&c| match client_manager.get(c) {
|
||||
Some(state) if !state.alive => Some(c),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
// we may not be receiving anymore but we should respond
|
||||
// to the original state and not the "new" one
|
||||
if receiving {
|
||||
for c in unresponsive_clients {
|
||||
log::warn!("device not responding, releasing keys!");
|
||||
let _ = consumer_notify.send(ConsumerEvent::ReleaseKeys(c)).await;
|
||||
}
|
||||
} else {
|
||||
// release pointer if the active client has not responded
|
||||
if !unresponsive_clients.is_empty() {
|
||||
log::warn!("client not responding, releasing pointer!");
|
||||
server.state.replace(State::Receiving);
|
||||
let _ = producer_notify.send(ProducerEvent::Release).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
ping_task
|
||||
}
|
||||
152
src/server/producer_task.rs
Normal file
152
src/server/producer_task.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::StreamExt;
|
||||
use std::{collections::HashSet, net::SocketAddr};
|
||||
|
||||
use tokio::{sync::mpsc::Sender, task::JoinHandle};
|
||||
|
||||
use crate::{
|
||||
client::{ClientEvent, ClientHandle},
|
||||
event::{Event, KeyboardEvent},
|
||||
producer::EventProducer,
|
||||
scancode,
|
||||
server::State,
|
||||
};
|
||||
|
||||
use super::Server;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum ProducerEvent {
|
||||
/// producer must release the mouse
|
||||
Release,
|
||||
/// producer is notified of a change in client states
|
||||
ClientEvent(ClientEvent),
|
||||
/// termination signal
|
||||
Terminate,
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
mut producer: Box<dyn EventProducer>,
|
||||
server: Server,
|
||||
sender_tx: Sender<(Event, SocketAddr)>,
|
||||
timer_tx: Sender<()>,
|
||||
release_bind: Vec<scancode::Linux>,
|
||||
) -> (JoinHandle<Result<()>>, Sender<ProducerEvent>) {
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
|
||||
let task = tokio::task::spawn_local(async move {
|
||||
let mut pressed_keys = HashSet::new();
|
||||
loop {
|
||||
tokio::select! {
|
||||
event = producer.next() => {
|
||||
match event {
|
||||
Some(Ok(event)) => handle_producer_event(&server, &mut producer, &sender_tx, &timer_tx, event, &mut pressed_keys, &release_bind).await?,
|
||||
Some(Err(e)) => return Err(anyhow!("event producer: {e:?}")),
|
||||
None => return Err(anyhow!("event producer closed")),
|
||||
}
|
||||
}
|
||||
e = rx.recv() => {
|
||||
log::debug!("producer notify rx: {e:?}");
|
||||
match e {
|
||||
Some(e) => match e {
|
||||
ProducerEvent::Release => {
|
||||
producer.release()?;
|
||||
server.state.replace(State::Receiving);
|
||||
|
||||
}
|
||||
ProducerEvent::ClientEvent(e) => producer.notify(e)?,
|
||||
ProducerEvent::Terminate => break,
|
||||
},
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
anyhow::Ok(())
|
||||
});
|
||||
(task, tx)
|
||||
}
|
||||
|
||||
fn update_pressed_keys(pressed_keys: &mut HashSet<scancode::Linux>, key: u32, state: u8) {
|
||||
if let Ok(scancode) = scancode::Linux::try_from(key) {
|
||||
log::debug!("key: {key}, state: {state}, scancode: {scancode:?}");
|
||||
match state {
|
||||
1 => pressed_keys.insert(scancode),
|
||||
_ => pressed_keys.remove(&scancode),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_producer_event(
|
||||
server: &Server,
|
||||
producer: &mut Box<dyn EventProducer>,
|
||||
sender_tx: &Sender<(Event, SocketAddr)>,
|
||||
timer_tx: &Sender<()>,
|
||||
event: (ClientHandle, Event),
|
||||
pressed_keys: &mut HashSet<scancode::Linux>,
|
||||
release_bind: &[scancode::Linux],
|
||||
) -> Result<()> {
|
||||
let (c, mut e) = event;
|
||||
log::trace!("({c}) {e:?}");
|
||||
|
||||
if let Event::Keyboard(KeyboardEvent::Key { key, state, .. }) = e {
|
||||
update_pressed_keys(pressed_keys, key, state);
|
||||
log::debug!("{pressed_keys:?}");
|
||||
if release_bind.iter().all(|k| pressed_keys.contains(k)) {
|
||||
pressed_keys.clear();
|
||||
log::info!("releasing pointer");
|
||||
producer.release()?;
|
||||
server.state.replace(State::Receiving);
|
||||
log::trace!("STATE ===> Receiving");
|
||||
// send an event to release all the modifiers
|
||||
e = Event::Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
let (addr, enter, start_timer) = {
|
||||
let mut enter = false;
|
||||
let mut start_timer = false;
|
||||
|
||||
// get client state for handle
|
||||
let mut client_manager = server.client_manager.borrow_mut();
|
||||
let client_state = match client_manager.get_mut(c) {
|
||||
Some(state) => state,
|
||||
None => {
|
||||
// should not happen
|
||||
log::warn!("unknown client!");
|
||||
producer.release()?;
|
||||
server.state.replace(State::Receiving);
|
||||
log::trace!("STATE ===> Receiving");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
// if we just entered the client we want to send additional enter events until
|
||||
// we get a leave event
|
||||
if let Event::Enter() = e {
|
||||
server.state.replace(State::AwaitingLeave);
|
||||
server
|
||||
.active_client
|
||||
.replace(Some(client_state.client.handle));
|
||||
log::trace!("Active client => {}", client_state.client.handle);
|
||||
start_timer = true;
|
||||
log::trace!("STATE ===> AwaitingLeave");
|
||||
enter = true;
|
||||
} else {
|
||||
// ignore any potential events in receiving mode
|
||||
if server.state.get() == State::Receiving && e != Event::Disconnect() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
(client_state.active_addr, enter, start_timer)
|
||||
};
|
||||
if start_timer {
|
||||
let _ = timer_tx.try_send(());
|
||||
}
|
||||
if let Some(addr) = addr {
|
||||
if enter {
|
||||
let _ = sender_tx.send((Event::Enter(), addr)).await;
|
||||
}
|
||||
let _ = sender_tx.send((e, addr)).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
40
src/server/resolver_task.rs
Normal file
40
src/server/resolver_task.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use tokio::{sync::mpsc::Sender, task::JoinHandle};
|
||||
|
||||
use crate::{client::ClientHandle, dns::DnsResolver};
|
||||
|
||||
use super::Server;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DnsRequest {
|
||||
pub hostname: String,
|
||||
pub handle: ClientHandle,
|
||||
}
|
||||
|
||||
pub fn new(resolver: DnsResolver, server: Server) -> (JoinHandle<()>, Sender<DnsRequest>) {
|
||||
let (dns_tx, mut dns_rx) = tokio::sync::mpsc::channel::<DnsRequest>(32);
|
||||
let resolver_task = tokio::task::spawn_local(async move {
|
||||
loop {
|
||||
let (host, handle) = match dns_rx.recv().await {
|
||||
Some(r) => (r.hostname, r.handle),
|
||||
None => break,
|
||||
};
|
||||
let ips = match resolver.resolve(&host).await {
|
||||
Ok(ips) => ips,
|
||||
Err(e) => {
|
||||
log::warn!("could not resolve host '{host}': {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if let Some(state) = server.client_manager.borrow_mut().get_mut(handle) {
|
||||
let mut addrs = HashSet::from_iter(state.client.fix_ips.iter().cloned());
|
||||
for ip in ips {
|
||||
addrs.insert(ip);
|
||||
}
|
||||
state.client.ips = addrs;
|
||||
}
|
||||
}
|
||||
});
|
||||
(resolver_task, dns_tx)
|
||||
}
|
||||
Reference in New Issue
Block a user