Compare commits

...

92 Commits

Author SHA1 Message Date
Ferdinand Schober
6a6d9a9fa9 chore: Release lan-mouse version 0.5.0 2023-12-28 17:57:04 +01:00
Ferdinand Schober
0fffd5bb15 fix error handling in portchange 2023-12-27 23:44:40 +01:00
Ferdinand Schober
b0df901fcc ci: rename lan-mouse.zip to lan-mouse-windows.zip 2023-12-27 21:58:06 +01:00
Ferdinand Schober
9c0cc98dc0 Remove duplicate code (#60) 2023-12-27 21:40:10 +01:00
Ferdinand Schober
5c152b0cbe ci: fix name-clash with macos binary 2023-12-27 21:39:22 +01:00
Ferdinand Schober
e155819542 unix: send SIGTERM instead of killing the service (#59) 2023-12-27 19:56:43 +01:00
Ferdinand Schober
4b6faea93a add missing exe 2023-12-24 20:47:21 +01:00
Ferdinand Schober
80d8a496bb zip windows files 2023-12-24 20:09:28 +01:00
Ferdinand Schober
53e1af0780 include dlls in release 2023-12-24 18:54:40 +01:00
Ferdinand Schober
d3fed1b769 enable gtk frontend in windows (#58)
The gtk frontend can now be built in windows!
The github workflow is updated to build GTK and add it to the releases section.
2023-12-24 18:00:59 +01:00
Ferdinand Schober
cdd3a3b818 Split tasks - event loop now properly asynchronous (#57)
DNS, etc. does no longer block the event loop
2023-12-23 14:46:38 +01:00
Ferdinand Schober
1cefa38543 hotfix: Dont stall the event loop if udp blocks 2023-12-22 17:45:20 +01:00
Ferdinand Schober
fed8e02d9f Update dependencies (#56)
update wayland-client + reis
2023-12-22 14:50:12 +01:00
Ferdinand Schober
65a12735e2 add missing release() on event producer 2023-12-22 13:32:39 +01:00
Ferdinand Schober
3484cab28c Update Roadmap 2023-12-20 11:35:49 +01:00
Ferdinand Schober
256d2107bd downgrade libadwaita
AdwToolbarView is available starting with libadwaita 1.4,
which is very bleeding-edge
2023-12-19 22:09:12 +01:00
Ferdinand Schober
3ac738fb52 fix formatting 2023-12-19 21:29:38 +01:00
Ferdinand Schober
2ac9277fb0 macos: impl relative mouse motion
this fixes mouse movement in games
2023-12-19 21:25:29 +01:00
Ferdinand Schober
d9fa86ef00 cli: wait for connection (#55) 2023-12-19 17:22:39 +01:00
Ferdinand Schober
015facec39 formatting 2023-12-18 21:41:43 +01:00
Ferdinand Schober
66de3e3cbc fix remaining clippy lints 2023-12-18 21:38:12 +01:00
Ferdinand Schober
4600db7af8 add cargo fmt + cargo clippy to rust workflow 2023-12-18 21:27:53 +01:00
Ferdinand Schober
9f23e1a75d remove unused code 2023-12-18 20:54:06 +01:00
Ferdinand Schober
a24b231e3c Update README.md 2023-12-18 18:14:28 +01:00
Ferdinand Schober
8de6c9bb87 Implement keycode translation for windows (#54)
closes #48 
closes #16
2023-12-18 18:08:10 +01:00
Ferdinand Schober
6766886377 Fix Keycodes in X11
Keycodes are now correctly offset for X11
2023-12-18 09:52:42 +01:00
Ferdinand Schober
a6ab109fae remove kde-fake-input backend 2023-12-17 19:33:40 +01:00
Ferdinand Schober
06c4e92d43 dont panic on unavailable compositor 2023-12-17 19:33:40 +01:00
Ferdinand Schober
f5a0ff4f3a Add a dummy backend 2023-12-17 19:33:40 +01:00
Ferdinand Schober
eca367cdb4 Fix an issue where client could not be entered again (#51)
RELEASE_MODIFIERS are now handled server side.

A client exited through CTRL+ALT+SHIFT+SUPER could sometimes not be entered again when the client
did not send a Modifiers (0) event.

Such a client would always respond with Modifiers (RELEASE_MODIFIERS) and immediately cause the sender to
exit again.

To prevent this, a modifier event with all modifiers released is now sent instead when the release modifiers are detected.
2023-12-17 17:57:17 +01:00
Ferdinand Schober
735434438f add leave event to make entering a client more reliable (#50)
Instead of relying on release events not getting lost, every event now signals the opponent
to release its pointer grab.

There is one case that requires a Leave event:

Consider a Sending client A and receiving Client B.

If B enters the dead-zone of client A, it will send an enter event towards A but before
A receives the Release event, it may still send additional events towards B that should not cause
B to immediately revert to Receiving state again.

Therefore B puts itself into AwaitingLeave state until it receives a Leave event coming from A.
A responds to the Enter event coming from B with a leave event, to signify that it will no longer
send any events and releases it's pointer.

To guard against packet loss of the leave events, B sends additional enter events while it is in AwaitingLeave
mode until it receives a Leave event at some point.

This is still not resilient against possible packet reordering in UDP but in the (rare) case where a leave event arrives before some other event coming from A, the user would simply need to move the pointer into the dead-zone again.
2023-12-17 17:38:06 +01:00
Ferdinand Schober
02d1b33e45 remove unused warnings 2023-12-17 16:08:18 +01:00
Ferdinand Schober
19143b90a5 add debug information to xdgrdp backend 2023-12-17 12:23:03 +01:00
Ferdinand Schober
ad2aeae275 make libc optional 2023-12-16 13:08:24 +01:00
Ferdinand Schober
0bdb1bc753 Update README 2023-12-16 12:14:07 +01:00
Ferdinand Schober
9140f60c69 X11: impl keyboard events (still disabled)
Needs keycode translation
2023-12-16 12:11:43 +01:00
Ferdinand Schober
0fbf9f4dc2 X11: Mouse emulation now fully supported 2023-12-16 11:48:06 +01:00
Ferdinand Schober
f13e25af82 fix: Dont release mouse on all events
When received motion events lead to entering the dead zone,
we set the state to sending but there may still be motion
events on the way so we must ignore those.
2023-12-16 10:13:14 +01:00
Ferdinand Schober
48f7ad3592 simplify enumerate 2023-12-15 20:47:51 +01:00
Ferdinand Schober
5fc02d471b cleanup server code 2023-12-15 20:24:14 +01:00
Ferdinand Schober
0c275bc2de remove duplicate log messages when ignoring events 2023-12-15 08:29:30 +01:00
Ferdinand Schober
010db79918 Update version number 2023-12-11 11:39:35 +01:00
Ferdinand Schober
ebf5a64f20 address clippy lints 2023-12-11 11:39:20 +01:00
CupricReki
5c8ea25563 Added example .desktop file. (#40)
* Added example .desktop file.

* remove Path (not required anymore)

* remove "Test" from name

* add German name

* Update description to match GitHub description

---------

Co-authored-by: Ferdinand Schober <ferdinandschober20@gmail.com>
2023-12-10 23:15:53 +01:00
Ferdinand Schober
b472b56b10 fix a compiler warning 2023-12-10 14:26:05 +01:00
Ferdinand Schober
18edb0dbad update screenshot light / dark 2023-12-10 13:17:32 +01:00
Ferdinand Schober
622b820c7f chore: Release lan-mouse version 0.4.0 2023-12-09 02:13:26 +01:00
Ferdinand Schober
09bf535eec Update README.md 2023-12-09 02:06:42 +01:00
Ferdinand Schober
39acce8e6a Update README.md 2023-12-09 02:03:18 +01:00
Ferdinand Schober
e3f9947284 macos: enable running lan-mouse on macos (#42)
* macos: initial support

- adapted conditional compilation
- moved lan-mouse socket to ~/Library/Caches/lan-mouse-socket.sock instead of XDG_RUNTIME_DIR
- support for mouse input emulation
TODO: Keycode translation, input capture
2023-12-09 01:35:08 +01:00
Ferdinand Schober
5a7e0cf89c formatting 2023-12-09 00:43:54 +01:00
Ferdinand Schober
56e5f7a30d Background service (#43)
better handling of background-service: lan-mouse can now be run without a gui by specifying --daemon as an argument.
Otherwise the servic will be run as a child process and correctly terminate when the window is closed / frontend exits.

Closes #38
2023-12-09 00:36:01 +01:00
Ferdinand Schober
9b242f6138 update README 2023-12-04 16:24:35 +01:00
Ferdinand Schober
b01f7c2793 move server to src/ 2023-12-03 22:37:41 +01:00
Ferdinand Schober
61b23c910b update README 2023-12-03 22:34:11 +01:00
Ferdinand Schober
74eebc07d8 Libei support - input emulation (#33)
Add support for input emulation through libei!
2023-12-03 21:56:01 +01:00
Ferdinand Schober
e6677c3061 Respect XDG_CONFIG_HOME for config.toml location (#41)
* Respect XDG_CONFIG_HOME for config.toml location
* add option to specify config via commandline

closes #39
2023-12-01 11:16:56 +01:00
Ferdinand Schober
e88241e816 port changing functionality (#34)
* port changing functionality

* add portchange to cli frontend
2023-10-17 15:12:17 +02:00
Ferdinand Schober
60a73b3cb0 Update README.md 2023-10-16 11:57:59 +02:00
Ferdinand Schober
cc28827721 Update README.md 2023-10-15 14:43:55 +02:00
Ferdinand Schober
dd1fb29f51 Update README.md (#32) 2023-10-15 14:43:18 +02:00
Ferdinand Schober
be0fe9f2d9 Support event consumer on KDE! (portal backend) (#31)
* Support event consumer on KDE! (portal backend)

Support for KDE event emulation using the remote-desktop xdg-desktop-portal

* fix scrolling (TODO: smooth / kinetic scrolling)

* windows: fix compilation errors

* Update README.md
2023-10-13 13:57:33 +02:00
Ferdinand Schober
4cdc5ea49c windows: fix compilation error 2023-10-12 12:48:39 +02:00
Ferdinand Schober
96ab7d304b wlroots: Fix crash when socket is overwhelmed
Previously when the output buffer was overwhelmed, additional
events were submitted until the outgoing buffer filled up, which
causes the wayland-connection to 'break' and not accept further attempts
to flush() the socket.
2023-10-12 12:40:57 +02:00
Ferdinand Schober
ab2514e508 Async (#30)
- manual eventloop now replaced by asycn-await using the tokio runtime
- dns no longer blocks the event loop
- simplifies logic
- makes xdg-desktop-portal easier to integrate
2023-10-11 14:52:18 +02:00
Ferdinand Schober
d4d6f05802 chore: Release lan-mouse version 0.3.3 2023-10-11 14:32:18 +02:00
Ferdinand Schober
79fa42b74e Update README.md 2023-09-30 16:29:15 +02:00
Ferdinand Schober
851b6d60eb Avoid sending frame events (#29)
* Avoid sending frame events

Frame events are now implicit - each network event implies a frame event
TODO: Accumulate correctly

* remove trace logs from producer
2023-09-28 13:01:38 +02:00
Ferdinand Schober
06725f4b14 Frontend improvement (#27)
* removed redundant dns lookups
* frontend now correctly reflects the state of the backend
* config.toml is loaded when starting gtk frontend
2023-09-25 13:03:17 +02:00
Ferdinand Schober
603646c799 Add LM_DEBUG_LAYER_SHELL environment variable
setting LM_DEBUG_LAYER_SHELL to a value will
make the indicators visible
2023-09-21 18:23:01 +02:00
Ferdinand Schober
b2179e88de adjust window size 2023-09-21 13:59:18 +02:00
Ferdinand Schober
bae52eb9e7 chore: Release lan-mouse version 0.3.2 2023-09-21 13:23:45 +02:00
Ferdinand Schober
0fbd09b07f fix 1px gap 2023-09-21 13:22:23 +02:00
Ferdinand Schober
96dd9c05a1 fix interference with swaybar 2023-09-21 12:57:51 +02:00
Ferdinand Schober
15c02ac505 chore: Release lan-mouse version 0.3.1 2023-09-21 12:37:00 +02:00
Ferdinand Schober
08893a39be fix incorrect orientation of layer surfaces
top and bottom surfaces were not sized & oriented correctly
closes #3
2023-09-21 12:35:27 +02:00
Ferdinand Schober
48b701b726 remove an unused import 2023-09-21 00:13:53 +02:00
Ferdinand Schober
891e21d3e9 read all output globals 2023-09-21 00:12:11 +02:00
Ferdinand Schober
6a5de3f025 Update README.md (#25)
scale image
2023-09-20 15:40:19 +02:00
Ferdinand Schober
1eb12baf15 chore: Release lan-mouse version 0.3.0 2023-09-20 15:31:53 +02:00
Ferdinand Schober
65048abcfc Update README.md (#24) 2023-09-20 15:25:09 +02:00
Ferdinand Schober
d042c0aa4a Libadwaita gui (#19)
Major Update: Functional GUI Frontend!
2023-09-20 15:23:33 +02:00
Ferdinand Schober
c50b746816 fix 2023-09-19 21:13:17 +02:00
Ferdinand Schober
3b09abb532 unlink socket in case it's left over from a crash 2023-09-19 21:11:47 +02:00
Ferdinand Schober
b839097cb2 chore: Release lan-mouse version 0.2.1 2023-09-19 19:47:55 +02:00
Ferdinand Schober
61b22fff51 fix a crash 2023-09-19 19:46:43 +02:00
Ferdinand Schober
4a61ed82a9 chore: Release lan-mouse version 0.2.0 2023-09-19 19:41:44 +02:00
Ferdinand Schober
a534f366b4 update dependencies 2023-09-19 19:41:05 +02:00
Ferdinand Schober
16311f8ae6 fix interrupted syscall when waking from suspend (#23) 2023-09-19 19:33:04 +02:00
Ferdinand Schober
1a4d0e05be Epoll (#20)
major update:
- remove threading overhead by resorting to an event driven design with mio as a backend for epoll
- Clients can now have an arbitrary amount of ip adresses and lan-mouse will automatically choose the correct one
- -> seemless switching between ethernet and wifi
- cli frontend + frontend adapter for future frontends
2023-09-19 19:12:47 +02:00
Ferdinand Schober
22e6c531af hotfix: Oneshot seems to crash Hyprland (#22)
closes #21
2023-09-17 14:32:47 +02:00
Ferdinand Schober
31eead5f8e continue without keymap (#18) 2023-09-12 12:17:44 +02:00
55 changed files with 8412 additions and 1716 deletions

View File

@@ -18,6 +18,7 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install libx11-dev libxtst-dev
sudo apt-get install libadwaita-1-dev libgtk-4-dev
- name: Release Build
run: cargo build --release
- name: Upload build artifact
@@ -29,18 +30,72 @@ jobs:
windows-release-build:
runs-on: windows-latest
steps:
- uses: actions/setup-python@v5
with:
python-version: '3.11'
# needed for cache restore
- name: create gtk dir
run: mkdir C:\gtk-build\gtk\x64\release
- uses: actions/cache@v3
id: cache
with:
path: c:/gtk-build/gtk/x64/release/**
key: gtk-windows-build
restore-keys: gtk-windows-build
- name: Update path
run: |
echo "PKG_CONFIG=C:\gtk-build\gtk\x64\release\bin\pkgconf.exe" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
echo "C:\pkg-config-lite-0.28-1\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
echo "C:\gtk-build\gtk\x64\release\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
echo $env:GITHUB_PATH
echo $env:PATH
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: |
# choco install msys2
# choco install visualstudio2022-workload-vctools
# choco install pkgconfiglite
pipx install gvsbuild
# see https://github.com/wingtk/gvsbuild/pull/1004
Move-Item "C:\Program Files\Git\usr\bin" "C:\Program Files\Git\usr\notbin"
Move-Item "C:\Program Files\Git\bin" "C:\Program Files\Git\notbin"
gvsbuild build --msys-dir=C:\msys64 gtk4 libadwaita librsvg
Move-Item "C:\Program Files\Git\usr\notbin" "C:\Program Files\Git\usr\bin"
Move-Item "C:\Program Files\Git\notbin" "C:\Program Files\Git\bin"
- uses: actions/checkout@v3
- name: Release Build
run: cargo build --release
- name: Create Archive
run: |
mkdir "lan-mouse-windows"
Get-Childitem -Path "C:\\gtk-build\\gtk\\x64\\release\\bin\\*.dll" -File -Recurse | Copy-Item -Destination "lan-mouse-windows"
Copy-Item -Path "target\release\lan-mouse.exe" -Destination "lan-mouse-windows"
Compress-Archive -Path "lan-mouse-windows\*" -DestinationPath lan-mouse-windows.zip
- name: Upload build artifact
uses: actions/upload-artifact@v3
with:
name: lan-mouse-windows
path: target/release/lan-mouse.exe
path: lan-mouse-windows.zip
macos-release-build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: install dependencies
run: brew install gtk4 libadwaita
- name: Release Build
run: |
cargo build --release
cp target/release/lan-mouse lan-mouse-macos-intel
- name: Upload build artifact
uses: actions/upload-artifact@v3
with:
name: lan-mouse-macos
path: lan-mouse-macos-intel
pre-release:
name: "Pre Release"
needs: [windows-release-build, linux-release-build]
needs: [windows-release-build, linux-release-build, macos-release-build]
runs-on: "ubuntu-latest"
steps:
- name: Download build artifacts
@@ -54,4 +109,5 @@ jobs:
title: "Development Build"
files: |
lan-mouse-linux/lan-mouse
lan-mouse-windows/lan-mouse.exe
lan-mouse-macos/lan-mouse-macos-intel
lan-mouse-windows/lan-mouse-windows.zip

View File

@@ -20,10 +20,15 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install libx11-dev libxtst-dev
sudo apt-get install libadwaita-1-dev libgtk-4-dev
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- name: Check Formatting
run: cargo fmt --check
- name: Clippy
run: cargo clippy --all-features --all-targets -- --deny warnings
- name: Upload build artifact
uses: actions/upload-artifact@v3
with:
@@ -36,12 +41,72 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v5
with:
python-version: '3.11'
# needed for cache restore
- name: create gtk dir
run: mkdir C:\gtk-build\gtk\x64\release
- uses: actions/cache@v3
id: cache
with:
path: c:/gtk-build/gtk/x64/release/**
key: gtk-windows-build
restore-keys: gtk-windows-build
- name: Update path
run: |
echo "PKG_CONFIG=C:\gtk-build\gtk\x64\release\bin\pkgconf.exe" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
echo "C:\pkg-config-lite-0.28-1\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
echo "C:\gtk-build\gtk\x64\release\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
echo $env:GITHUB_PATH
echo $env:PATH
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: |
# choco install msys2
# choco install visualstudio2022-workload-vctools
# choco install pkgconfiglite
pipx install gvsbuild
# see https://github.com/wingtk/gvsbuild/pull/1004
Move-Item "C:\Program Files\Git\usr\bin" "C:\Program Files\Git\usr\notbin"
Move-Item "C:\Program Files\Git\bin" "C:\Program Files\Git\notbin"
gvsbuild build --msys-dir=C:\msys64 gtk4 libadwaita librsvg
Move-Item "C:\Program Files\Git\usr\notbin" "C:\Program Files\Git\usr\bin"
Move-Item "C:\Program Files\Git\notbin" "C:\Program Files\Git\bin"
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- name: Check Formatting
run: cargo fmt --check
- name: Clippy
run: cargo clippy --all-features --all-targets -- --deny warnings
- name: Copy Gtk Dlls
run: Get-Childitem -Path "C:\\gtk-build\\gtk\\x64\\release\\bin\\*.dll" -File -Recurse | Copy-Item -Destination "target\\debug"
- name: Upload build artifact
uses: actions/upload-artifact@v3
with:
name: lan-mouse-windows
path: target/debug/lan-mouse.exe
path: |
target/debug/lan-mouse.exe
target/debug/*.dll
build-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: install dependencies
run: brew install gtk4 libadwaita
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- name: Check Formatting
run: cargo fmt --check
- name: Clippy
run: cargo clippy --all-features --all-targets -- --deny warnings
- name: Upload build artifact
uses: actions/upload-artifact@v3
with:
name: lan-mouse-macos
path: target/debug/lan-mouse

View File

@@ -14,6 +14,7 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install libx11-dev libxtst-dev
sudo apt-get install libadwaita-1-dev libgtk-4-dev
- name: Release Build
run: cargo build --release
- name: Upload build artifact
@@ -25,18 +26,72 @@ jobs:
windows-release-build:
runs-on: windows-latest
steps:
- uses: actions/setup-python@v5
with:
python-version: '3.11'
# needed for cache restore
- name: create gtk dir
run: mkdir C:\gtk-build\gtk\x64\release
- uses: actions/cache@v3
id: cache
with:
path: c:/gtk-build/gtk/x64/release/**
key: gtk-windows-build
restore-keys: gtk-windows-build
- name: Update path
run: |
echo "PKG_CONFIG=C:\gtk-build\gtk\x64\release\bin\pkgconf.exe" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
echo "C:\pkg-config-lite-0.28-1\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
echo "C:\gtk-build\gtk\x64\release\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
echo $env:GITHUB_PATH
echo $env:PATH
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: |
# choco install msys2
# choco install visualstudio2022-workload-vctools
# choco install pkgconfiglite
pipx install gvsbuild
# see https://github.com/wingtk/gvsbuild/pull/1004
Move-Item "C:\Program Files\Git\usr\bin" "C:\Program Files\Git\usr\notbin"
Move-Item "C:\Program Files\Git\bin" "C:\Program Files\Git\notbin"
gvsbuild build --msys-dir=C:\msys64 gtk4 libadwaita librsvg
Move-Item "C:\Program Files\Git\usr\notbin" "C:\Program Files\Git\usr\bin"
Move-Item "C:\Program Files\Git\notbin" "C:\Program Files\Git\bin"
- uses: actions/checkout@v3
- name: Release Build
run: cargo build --release
- name: Create Archive
run: |
mkdir "lan-mouse-windows"
Get-Childitem -Path "C:\\gtk-build\\gtk\\x64\\release\\bin\\*.dll" -File -Recurse | Copy-Item -Destination "lan-mouse-windows"
Copy-Item -Path "target\release\lan-mouse.exe" -Destination "lan-mouse-windows"
Compress-Archive -Path "lan-mouse-windows\*" -DestinationPath lan-mouse-windows.zip
- name: Upload build artifact
uses: actions/upload-artifact@v3
with:
name: lan-mouse-windows
path: target/release/lan-mouse.exe
path: lan-mouse-windows.zip
macos-release-build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: install dependencies
run: brew install gtk4 libadwaita
- name: Release Build
run: |
cargo build --release
cp target/release/lan-mouse lan-mouse-macos-intel
- name: Upload build artifact
uses: actions/upload-artifact@v3
with:
name: lan-mouse-macos
path: lan-mouse-macos-intel
tagged-release:
name: "Tagged Release"
needs: [windows-release-build, linux-release-build]
needs: [windows-release-build, linux-release-build, macos-release-build]
runs-on: "ubuntu-latest"
steps:
- name: Download build artifacts
@@ -48,4 +103,5 @@ jobs:
prerelease: false
files: |
lan-mouse-linux/lan-mouse
lan-mouse-windows/lan-mouse.exe
lan-mouse-macos/lan-mouse-macos-intel
lan-mouse-windows/lan-mouse-windows.zip

3
.gitignore vendored
View File

@@ -1 +1,4 @@
/target
.gdbinit
.idea/
.vs/

2078
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
[package]
name = "lan-mouse"
description = "Software KVM Switch / mouse & keyboard sharing software for Local Area Networks"
version = "0.1.1-alpha.1"
version = "0.5.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/ferdinandschober/lan-mouse"
@@ -13,29 +13,49 @@ strip = true
lto = "fat"
[dependencies]
tempfile = "3.6"
trust-dns-resolver = "0.22"
tempfile = "3.8"
trust-dns-resolver = "0.23"
memmap = "0.7"
toml = "0.7"
serde = "1.0"
serde_derive = "1.0"
toml = "0.8"
serde = { version = "1.0", features = ["derive"] }
anyhow = "1.0.71"
log = "0.4.20"
env_logger = "0.10.0"
serde_json = "1.0.107"
tokio = {version = "1.32.0", features = ["io-util", "macros", "net", "rt", "sync", "signal"] }
async-trait = "0.1.73"
futures-core = "0.3.28"
futures = "0.3.28"
clap = { version="4.4.11", features = ["derive"] }
gtk = { package = "gtk4", version = "0.7.2", features = ["v4_2"], optional = true }
adw = { package = "libadwaita", version = "0.5.2", features = ["v1_1"], optional = true }
async-channel = { version = "2.1.1", optional = true }
[target.'cfg(unix)'.dependencies]
wayland-client = { version="0.30.2", optional = true }
wayland-protocols = { version="0.30.0", features=["client", "staging", "unstable"], optional = true }
wayland-protocols-wlr = { version="0.1.0", features=["client"], optional = true }
wayland-protocols-misc = { version="0.1.0", features=["client"], optional = true }
wayland-protocols-plasma = { version="0.1.0", features=["client"], optional = true }
libc = "0.2.148"
[target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
wayland-client = { version="0.31.1", optional = true }
wayland-protocols = { version="0.31.0", features=["client", "staging", "unstable"], optional = true }
wayland-protocols-wlr = { version="0.2.0", features=["client"], optional = true }
wayland-protocols-misc = { version="0.2.0", features=["client"], optional = true }
x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
ashpd = { version = "0.6.2", default-features = false, features = ["tokio"], optional = true }
reis = { git = "https://github.com/ids1024/reis", features = [ "tokio" ], optional = true }
[target.'cfg(target_os="macos")'.dependencies]
core-graphics = { version = "0.23", features = ["highsierra"] }
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3.9", features = ["winuser"] }
[build-dependencies]
glib-build-tools = "0.18.0"
[features]
default = ["wayland", "x11", "xdg_desktop_portal", "libei"]
wayland = ["dep:wayland-client", "dep:wayland-protocols", "dep:wayland-protocols-wlr", "dep:wayland-protocols-misc", "dep:wayland-protocols-plasma"]
default = ["wayland", "x11", "xdg_desktop_portal", "libei", "gtk"]
wayland = ["dep:wayland-client", "dep:wayland-protocols", "dep:wayland-protocols-wlr", "dep:wayland-protocols-misc" ]
x11 = ["dep:x11"]
xdg_desktop_portal = []
libei = []
xdg_desktop_portal = ["dep:ashpd"]
libei = ["dep:reis", "dep:ashpd"]
gtk = ["dep:gtk", "dep:adw", "dep:async-channel"]

329
README.md
View File

@@ -1,164 +1,224 @@
# Lan Mouse Share
# Lan Mouse
Lan Mouse is a mouse and keyboard sharing software similar to universal-control on Apple devices.
It allows for using multiple pcs with a single set of mouse and keyboard.
This is also known as a Software KVM switch.
The primary target is Wayland on Linux but Windows and MacOS and Linux on Xorg have partial support as well (see below for more details).
- _Now with a gtk frontend_
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/feschber/lan-mouse/assets/40996949/016a06a9-76db-4951-9dcc-127d012c59df">
<source media="(prefers-color-scheme: light)" srcset="https://github.com/feschber/lan-mouse/assets/40996949/d6318340-f811-4e16-9d6e-d1b79883c709">
<img alt="Screenshot of Lan-Mouse" srcset="https://github.com/feschber/lan-mouse/assets/40996949/016a06a9-76db-4951-9dcc-127d012c59df">
</picture>
Goal of this project is to be an open-source replacement for proprietary tools like [Synergy](https://symless.com/synergy), [Share Mouse](https://www.sharemouse.com/de/).
Focus lies on performance and a clean, manageable implementation that can easily be expanded to support additional backends like e.g. Android, iOS, ... .
Of course ***blazingly fast™*** and stable, because it's written in rust.
***blazingly fast™*** because it's written in rust.
For an alternative (with slightly different goals) you may check out [Input Leap](https://github.com/input-leap).
## Configuration
Configuration is done through the file `config.toml`,
which must be located in the current working directory when
executing lan-mouse.
## OS Support
### Example config
A minimal config file could look like this:
The following table shows support for input emulation (to emulate events received from other clients) and
input capture (to send events *to* other clients) on different operating systems:
```toml
[left]
host_name = "my-laptop"
```
| Backend | input emulation | input capture |
|---------------------------|--------------------------|--------------------------------------|
| Wayland (wlroots) | :heavy_check_mark: | :heavy_check_mark: |
| Wayland (KDE) | :heavy_check_mark: | :heavy_check_mark: |
| Wayland (Gnome) | :heavy_check_mark: | WIP |
| X11 | :heavy_check_mark: | WIP |
| Windows | :heavy_check_mark: | WIP |
| MacOS | ( :heavy_check_mark: ) | WIP |
Where `left` can be either `left`, `right`, `top` or `bottom`.
### Additional options
Additionally
- a preferred backend
- a port override for the default port (4242)
can be specified.
Supported backends currently include "wlroots", "x11" and "windows".
These two options can also be specified via the commandline
options `--backend` and `--port` respectively.
Keycode translation is not yet implemented so on MacOS only mouse emulation works as of right now.
## Build and Run
Build only
### Install Dependencies
#### Macos
```sh
brew install libadwaita
```
#### Ubuntu and derivatives
```sh
sudo apt install libadwaita-1-dev libgtk-4-dev libx11-dev libxtst-dev
```
#### Arch and derivatives
```sh
sudo pacman -S libadwaita gtk libx11 libxtst
```
#### Fedora and derivatives
```sh
sudo dnf install libadwaita-devel libXtst-devel libX11-devel
```
#### Windows
Follow the instructions at [gtk-rs.org](https://gtk-rs.org/gtk4-rs/stable/latest/book/installation_windows.html)
*TLDR:*
Build gtk from source
- The following commands should be run in an admin power shell instance:
```sh
# install chocolatey
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
# install python 3.11 (Version is important, as 3.12 does not work currently)
choco install python --version=3.11.0
# install git
choco install git
# install msys2
choco install msys2
# install Visual Studio 2022
choco install visualstudio2022-workload-vctools
```
- The following commands should be run in a regular power shell instance:
```sh
# install gvsbuild with python
python -m pip install --user pipx
python -m pipx ensurepath
pipx install gvsbuild
# build gtk + libadwaita
gvsbuild build gtk4 libadwaita librsvg
```
Make sure to add the directory `C:\gtk-build\gtk\x64\release\bin`
[to the `PATH` environment variable]((https://learn.microsoft.com/en-us/previous-versions/office/developer/sharepoint-2010/ee537574(v=office.14))). Otherwise the project will fail to build.
To avoid building GTK from source, it is possible to disable
the gtk frontend (see conditional compilation below).
### Build and run
Build in release mode:
```sh
cargo build --release
```
Run
Run directly:
```sh
cargo run --release
```
### Conditional Compilation
Currently only x11, wayland and windows are supported backends,
Currently only x11, wayland, windows and MacOS are supported backends.
Depending on the toolchain used, support for other platforms is omitted
automatically (it does not make sense to build a Windows `.exe` with
support for x11 and wayland backends).
However one might still want to omit support for e.g. wayland or x11 on
However one might still want to omit support for e.g. wayland, x11 or libei on
a Linux system.
This is possible through
[cargo features](https://doc.rust-lang.org/cargo/reference/features.html)
[cargo features](https://doc.rust-lang.org/cargo/reference/features.html).
E.g. if only wayland support is needed, the following command produces
an executable with just support for wayland:
```sh
cargo build --no-default-features --features wayland
```
For a detailed list of available features, checkout the [Cargo.toml](./Cargo.toml)
## OS Support
## Usage
### Gtk Frontend
By default the gtk frontend will open when running `lan-mouse`.
The following table shows support for Event receiving and event Emitting
on different operating systems:
To add a new connection, simply click the `Add` button on *both* devices,
enter the corresponding hostname and activate it.
| Backend | Event Receiving | Event Emitting |
|---------------------------|--------------------------|--------------------------------------|
| Wayland (wlroots) | :heavy_check_mark: | :heavy_check_mark: |
| Wayland (KDE) | WIP | :heavy_check_mark: |
| Wayland (Gnome) | TODO (libei support) | TODO (wlr-layer-shell not supported) |
| X11 | WIP | TODO |
| Windows | needs improvements | TODO |
| MacOS | TODO (I dont own a Mac) | TODO (I dont own a Mac) |
If the mouse can not be moved onto a device, make sure you have port `4242` (or the one selected)
opened up in your firewall.
## Wayland compositor support
### Input Emulation (for receiving events)
On wayland input-emulation is in an early/unstable state as of writing this.
### Command Line Interface
The cli interface can be enabled using `--frontend cli` as commandline arguments.
Type `help` to list the available commands.
Different compositors have different ways of enabling input emulation:
E.g.:
```sh
$ cargo run --release -- --frontend cli
(...)
> connect <host> left|right|top|bottom
(...)
> list
(...)
> activate 0
```
Most wlroots-based compositors like Hyprland and Sway support the following
unstable wayland protocols for keyboard and mouse emulation:
- [virtual-keyboard-unstable-v1](https://wayland.app/protocols/virtual-keyboard-unstable-v1)
- [wlr-virtual-pointer-unstable-v1](https://wayland.app/protocols/wlr-virtual-pointer-unstable-v1) are used to emulate input on wlroots compositors
### Daemon
Lan Mouse can be launched in daemon mode to keep it running in the background.
To do so, add `--daemon` to the commandline args:
KDE also has a protocol for input emulation ([kde-fake-input](https://wayland.app/protocols/kde-fake-input)), it is however not exposed to
third party apps, so the recommended way of enabling input emulation in KDE is the
[freedesktop remote-desktop-portal](https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.RemoteDesktop).
```sh
$ cargo run --release -- --daemon
```
Gnome uses [libei](https://gitlab.freedesktop.org/libinput/libei) for input emulation,
which has the goal to become the general approach for emulating Input on wayland.
## Configuration
To automatically load clients on startup, the file `$XDG_CONFIG_HOME/lan-mouse/config.toml` is parsed.
`$XDG_CONFIG_HOME` defaults to `~/.config/`.
| Required Protocols (Event Receiving) | Sway | Kwin | Gnome |
|----------------------------------------|--------------------|----------------------|----------------------|
| wlr-virtual-pointer-unstable-v1 | :heavy_check_mark: | :x: | :x: |
| virtual-keyboard-unstable-v1 | :heavy_check_mark: | :x: | :x: |
| ~fake-input~ | :x: | ~:heavy_check_mark:~ | :x: |
To create this file you can copy the following example config:
### Input capture
### Example config
To capture mouse and keyboard input, a few things are necessary:
- Displaying an immovable surface at screen edges
- Locking the mouse in place
- (optionally but highly recommended) reading unaccelerated mouse input
```toml
# example configuration
| Required Protocols (Event Emitting) | Sway | Kwin | Gnome |
|----------------------------------------|--------------------|----------------------|----------------------|
| pointer-constraints-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| relative-pointer-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| keyboard-shortcuts-inhibit-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| wlr-layer-shell-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :x: |
# optional port (defaults to 4242)
port = 4242
# # optional frontend -> defaults to gtk if available
# # possible values are "cli" and "gtk"
# frontend = "gtk"
The [zwlr\_virtual\_pointer\_manager\_v1](wlr-virtual-pointer-unstable-v1) is required
to display surfaces on screen edges and used to display the immovable window on
both wlroots based compositors and KDE.
# define a client on the right side with host name "iridium"
[right]
# hostname
host_name = "iridium"
# optional list of (known) ip addresses
ips = ["192.168.178.156"]
Gnome unfortunately does not support this protocol
and [likely won't ever support it](https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/1141).
# define a client on the left side with IP address 192.168.178.189
[left]
# The hostname is optional: When no hostname is specified,
# at least one ip address needs to be specified.
host_name = "thorium"
# ips for ethernet and wifi
ips = ["192.168.178.189", "192.168.178.172"]
# optional port
port = 4242
```
So there is currently no way of doing this in Wayland, aside from a custom Gnome-Shell
extension, which is not a very elegant solution.
Where `left` can be either `left`, `right`, `top` or `bottom`.
This is to be looked into in the future.
~In order for layershell surfaces to be able to lock the pointer using the pointer\_constraints protocol [this patch](https://github.com/swaywm/sway/pull/7178) needs to be applied to sway.~
(this works natively on sway versions >= 1.8)
## Windows support
Currently windows can receive mouse and keyboard events, however unlike
with the wlroots back-end,
the scancodes are not translated between keyboard layouts.
Event emitting is WIP.
## TODOS
- [x] Capture the actual mouse events on the server side via a wayland client and send them to the client
- [x] Mouse grabbing
- [x] Window with absolute position -> wlr\_layer\_shell
- [x] DNS resolving
- [x] Keyboard support
- [x] Scrollwheel support
- [x] Button support
- [ ] Latency measurement + logging
- [ ] Bandwidth usage approximation + logging
- [ ] Multiple IP addresses -> check which one is reachable
- [x] Merge server and client -> Both client and server can send and receive events depending on what mouse is used where
- [ ] Liveness tracking (automatically ungrab mouse when client unreachable)
## Roadmap
- [x] Graphical frontend (gtk + libadwaita)
- [x] respect xdg-config-home for config file location.
- [x] IP Address switching
- [x] Liveness tracking Automatically ungrab mouse when client unreachable
- [ ] Liveness tracking: Automatically release keys, when server offline
- [ ] X11 Input Capture
- [ ] Windows Input Capture
- [ ] MacOS Input Capture
- [ ] MaxOS KeyCode Translation
- [ ] Latency measurement and visualization
- [ ] Bandwidth usage measurement and visualization
- [ ] Clipboard support
- [ ] Graphical frontend (gtk?)
- [ ] *Encrytion*
- [ ] Gnome Shell Extension (layer shell is not supported)
- [ ] respect xdg-config-home for config file location.
- [ ] *Encryption*
## Protocol
Currently *all* mouse and keyboard events are sent via **UDP** for performance reasons.
@@ -207,3 +267,54 @@ would be a better choice for the future and could also help for WIFI connections
Sending key and mouse event data over the local network might not be the biggest security concern but in any public network or business environment it's *QUITE* a problem to basically broadcast your keystrokes.
- There should be an encryption layer below the application to enable a secure link.
- The encryption keys could be generated by the graphical frontend.
## Wayland support
### Input Emulation (for receiving events)
On wayland input-emulation is in an early/unstable state as of writing this.
For this reason a suitable backend is chosen based on the active desktop environment / compositor.
Different compositors have different ways of enabling input emulation:
#### Wlroots
Most wlroots-based compositors like Hyprland and Sway support the following
unstable wayland protocols for keyboard and mouse emulation:
- [virtual-keyboard-unstable-v1](https://wayland.app/protocols/virtual-keyboard-unstable-v1)
- [wlr-virtual-pointer-unstable-v1](https://wayland.app/protocols/wlr-virtual-pointer-unstable-v1)
#### KDE
KDE also has a protocol for input emulation ([kde-fake-input](https://wayland.app/protocols/kde-fake-input)),
it is however not exposed to third party applications.
The recommended way to emulate input on KDE is the
[freedesktop remote-desktop-portal](https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.RemoteDesktop).
#### Gnome
Gnome uses [libei](https://gitlab.freedesktop.org/libinput/libei) for input emulation and capture,
which has the goal to become the general approach for emulating and capturing Input on Wayland.
### Input capture
To capture mouse and keyboard input, a few things are necessary:
- Displaying an immovable surface at screen edges
- Locking the mouse in place
- (optionally but highly recommended) reading unaccelerated mouse input
| Required Protocols (Event Emitting) | Sway | Kwin | Gnome |
|----------------------------------------|--------------------|----------------------|----------------------|
| pointer-constraints-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| relative-pointer-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| keyboard-shortcuts-inhibit-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| wlr-layer-shell-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :x: |
The [zwlr\_virtual\_pointer\_manager\_v1](wlr-virtual-pointer-unstable-v1) is required
to display surfaces on screen edges and used to display the immovable window on
both wlroots based compositors and KDE.
Gnome unfortunately does not support this protocol
and [likely won't ever support it](https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/1141).
~In order for layershell surfaces to be able to lock the pointer using the pointer\_constraints protocol [this patch](https://github.com/swaywm/sway/pull/7178) needs to be applied to sway.~
(this works natively on sway versions >= 1.8)

8
build.rs Normal file
View File

@@ -0,0 +1,8 @@
fn main() {
// composite_templates
glib_build_tools::compile_resources(
&["resources"],
"resources/resources.gresource.xml",
"lan-mouse.gresource",
);
}

View File

@@ -1,22 +1,23 @@
# example configuration
# optional port
# optional port (defaults to 4242)
port = 4242
# optional backend override
backend = "wlroots"
# optional frontend -> defaults to gtk if available
# frontend = "gtk"
# define a client on the right side with host name "iridium"
[right]
# hostname
host_name = "iridium"
# optional ip address
ip = "192.168.178.141"
# optional port (defaults to 4242)
port = 4242
# 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
#
# when an IP address is specified, it takes priority
# and host_name can be omitted
[left]
ip = "192.168.178.189"
# 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

14
de.feschber.LanMouse.yml Normal file
View File

@@ -0,0 +1,14 @@
app-id: de.feschber.LanMouse
runtime: org.freedesktop.Platform
runtime-version: '22.08'
sdk: org.freedesktop.Sdk
command: target/release/lan-mouse
modules:
- name: hello
buildsystem: simple
build-commands:
- cargo build --release
- install -D lan-mouse /app/bin/lan-mouse
sources:
- type: file
path: target/release/lan-mouse

272
deny.toml Normal file
View File

@@ -0,0 +1,272 @@
# This template contains all of the possible sections and their default values
# Note that all fields that take a lint level have these possible values:
# * deny - An error will be produced and the check will fail
# * warn - A warning will be produced, but the check will not fail
# * allow - No warning or error will be produced, though in some cases a note
# will be
# The values provided in this template are the default values that will be used
# when any section or field is not specified in your own configuration
# Root options
# If 1 or more target triples (and optionally, target_features) are specified,
# only the specified targets will be checked when running `cargo deny check`.
# This means, if a particular package is only ever used as a target specific
# dependency, such as, for example, the `nix` crate only being used via the
# `target_family = "unix"` configuration, that only having windows targets in
# this list would mean the nix crate, as well as any of its exclusive
# dependencies not shared by any other crates, would be ignored, as the target
# list here is effectively saying which targets you are building for.
targets = [
# The triple can be any string, but only the target triples built in to
# rustc (as of 1.40) can be checked against actual config expressions
#{ triple = "x86_64-unknown-linux-musl" },
# You can also specify which target_features you promise are enabled for a
# particular target. target_features are currently not validated against
# the actual valid features supported by the target architecture.
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
]
# When creating the dependency graph used as the source of truth when checks are
# executed, this field can be used to prune crates from the graph, removing them
# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate
# is pruned from the graph, all of its dependencies will also be pruned unless
# they are connected to another crate in the graph that hasn't been pruned,
# so it should be used with care. The identifiers are [Package ID Specifications]
# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html)
#exclude = []
# If true, metadata will be collected with `--all-features`. Note that this can't
# be toggled off if true, if you want to conditionally enable `--all-features` it
# is recommended to pass `--all-features` on the cmd line instead
all-features = false
# If true, metadata will be collected with `--no-default-features`. The same
# caveat with `all-features` applies
no-default-features = false
# If set, these feature will be enabled when collecting metadata. If `--features`
# is specified on the cmd line they will take precedence over this option.
#features = []
# When outputting inclusion graphs in diagnostics that include features, this
# option can be used to specify the depth at which feature edges will be added.
# This option is included since the graphs can be quite large and the addition
# of features from the crate(s) to all of the graph roots can be far too verbose.
# This option can be overridden via `--feature-depth` on the cmd line
feature-depth = 1
# This section is considered when running `cargo deny check advisories`
# More documentation for the advisories section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
[advisories]
# The path where the advisory database is cloned/fetched into
db-path = "~/.cargo/advisory-db"
# The url(s) of the advisory databases to use
db-urls = ["https://github.com/rustsec/advisory-db"]
# The lint level for security vulnerabilities
vulnerability = "deny"
# The lint level for unmaintained crates
unmaintained = "warn"
# The lint level for crates that have been yanked from their source registry
yanked = "warn"
# The lint level for crates with security notices. Note that as of
# 2019-12-17 there are no security notice advisories in
# https://github.com/rustsec/advisory-db
notice = "warn"
# A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered.
ignore = [
#"RUSTSEC-0000-0000",
]
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
# lower than the range specified will be ignored. Note that ignored advisories
# will still output a note when they are encountered.
# * None - CVSS Score 0.0
# * Low - CVSS Score 0.1 - 3.9
# * Medium - CVSS Score 4.0 - 6.9
# * High - CVSS Score 7.0 - 8.9
# * Critical - CVSS Score 9.0 - 10.0
#severity-threshold =
# If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library.
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
# See Git Authentication for more information about setting up git authentication.
#git-fetch-with-cli = true
# This section is considered when running `cargo deny check licenses`
# More documentation for the licenses section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
[licenses]
# The lint level for crates which do not have a detectable license
unlicensed = "deny"
# List of explicitly allowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
allow = [
"MIT",
"BSD-3-Clause",
"ISC",
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"Unicode-DFS-2016",
]
# List of explicitly disallowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
deny = [
#"Nokia",
]
# Lint level for licenses considered copyleft
copyleft = "warn"
# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses
# * both - The license will be approved if it is both OSI-approved *AND* FSF
# * either - The license will be approved if it is either OSI-approved *OR* FSF
# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF
# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved
# * neither - This predicate is ignored and the default lint level is used
allow-osi-fsf-free = "neither"
# Lint level used when no other predicates are matched
# 1. License isn't in the allow or deny lists
# 2. License isn't copyleft
# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither"
default = "deny"
# The confidence threshold for detecting a license from license text.
# The higher the value, the more closely the license text must be to the
# canonical license text of a valid SPDX license file.
# [possible values: any between 0.0 and 1.0].
confidence-threshold = 0.8
# Allow 1 or more licenses on a per-crate basis, so that particular licenses
# aren't accepted for every possible crate as with the normal allow list
exceptions = [
# Each entry is the crate and version constraint, and its specific allow
# list
#{ allow = ["Zlib"], name = "adler32", version = "*" },
]
# Some crates don't have (easily) machine readable licensing information,
# adding a clarification entry for it allows you to manually specify the
# licensing information
#[[licenses.clarify]]
# The name of the crate the clarification applies to
#name = "ring"
# The optional version constraint for the crate
#version = "*"
# The SPDX expression for the license requirements of the crate
#expression = "MIT AND ISC AND OpenSSL"
# One or more files in the crate's source used as the "source of truth" for
# the license expression. If the contents match, the clarification will be used
# when running the license check, otherwise the clarification will be ignored
# and the crate will be checked normally, which may produce warnings or errors
# depending on the rest of your configuration
#license-files = [
# Each entry is a crate relative path, and the (opaque) hash of its contents
#{ path = "LICENSE", hash = 0xbd0eed23 }
#]
[licenses.private]
# If true, ignores workspace crates that aren't published, or are only
# published to private registries.
# To see how to mark a crate as unpublished (to the official registry),
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
ignore = false
# One or more private registries that you might publish crates to, if a crate
# is only published to private registries, and ignore is true, the crate will
# not have its license(s) checked
registries = [
#"https://sekretz.com/registry
]
# This section is considered when running `cargo deny check bans`.
# More documentation about the 'bans' section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
[bans]
# Lint level for when multiple versions of the same crate are detected
multiple-versions = "warn"
# Lint level for when a crate version requirement is `*`
wildcards = "allow"
# The graph highlighting used when creating dotgraphs for crates
# with multiple versions
# * lowest-version - The path to the lowest versioned duplicate is highlighted
# * simplest-path - The path to the version with the fewest edges is highlighted
# * all - Both lowest-version and simplest-path are used
highlight = "all"
# The default lint level for `default` features for crates that are members of
# the workspace that is being checked. This can be overriden by allowing/denying
# `default` on a crate-by-crate basis if desired.
workspace-default-features = "allow"
# The default lint level for `default` features for external crates that are not
# members of the workspace. This can be overriden by allowing/denying `default`
# on a crate-by-crate basis if desired.
external-default-features = "allow"
# List of crates that are allowed. Use with care!
allow = [
#{ name = "ansi_term", version = "=0.11.0" },
]
# List of crates to deny
deny = [
# Each entry the name of a crate and a version range. If version is
# not specified, all versions will be matched.
#{ name = "ansi_term", version = "=0.11.0" },
#
# Wrapper crates can optionally be specified to allow the crate when it
# is a direct dependency of the otherwise banned crate
#{ name = "ansi_term", version = "=0.11.0", wrappers = [] },
]
# List of features to allow/deny
# Each entry the name of a crate and a version range. If version is
# not specified, all versions will be matched.
#[[bans.features]]
#name = "reqwest"
# Features to not allow
#deny = ["json"]
# Features to allow
#allow = [
# "rustls",
# "__rustls",
# "__tls",
# "hyper-rustls",
# "rustls",
# "rustls-pemfile",
# "rustls-tls-webpki-roots",
# "tokio-rustls",
# "webpki-roots",
#]
# If true, the allowed features must exactly match the enabled feature set. If
# this is set there is no point setting `deny`
#exact = true
# Certain crates/versions that will be skipped when doing duplicate detection.
skip = [
#{ name = "ansi_term", version = "=0.11.0" },
]
# Similarly to `skip` allows you to skip certain crates during duplicate
# detection. Unlike skip, it also includes the entire tree of transitive
# dependencies starting at the specified crate, up to a certain depth, which is
# by default infinite.
skip-tree = [
#{ name = "ansi_term", version = "=0.11.0", depth = 20 },
]
# This section is considered when running `cargo deny check sources`.
# More documentation about the 'sources' section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
[sources]
# Lint level for what to happen when a crate from a crate registry that is not
# in the allow list is encountered
unknown-registry = "warn"
# Lint level for what to happen when a crate from a git repository that is not
# in the allow list is encountered
unknown-git = "warn"
# List of URLs for allowed crate registries. Defaults to the crates.io index
# if not specified. If it is specified but empty, no registries are allowed.
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
# List of URLs for allowed Git repositories
allow-git = []
[sources.allow-org]
# 1 or more github.com organizations to allow git sources for
github = [""]
# 1 or more gitlab.com organizations to allow git sources for
gitlab = [""]
# 1 or more bitbucket.org organizations to allow git sources for
bitbucket = [""]

13
lan-mouse.desktop Normal file
View File

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

74
resources/client_row.ui Normal file
View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="ClientRow" parent="AdwExpanderRow">
<property name="title">hostname</property>
<!-- enabled -->
<child type="prefix">
<object class="GtkSwitch" id="enable_switch">
<signal name="state_set" handler="handle_client_set_state" swapped="true"/>
<property name="valign">center</property>
<property name="halign">end</property>
<property name="tooltip-text" translatable="yes">enable</property>
</object>
</child>
<!-- host -->
<child>
<object class="AdwActionRow">
<property name="title">hostname</property>
<property name="subtitle">port</property>
<!-- hostname -->
<child>
<object class="GtkEntry" id="hostname">
<!-- <property name="title" translatable="yes">hostname</property> -->
<property name="xalign">0.5</property>
<property name="valign">center</property>
<property name="placeholder-text">hostname</property>
<property name="width-chars">-1</property>
</object>
</child>
<!-- port -->
<child>
<object class="GtkEntry" id="port">
<!-- <property name="title" translatable="yes">port</property> -->
<property name="input_purpose">GTK_INPUT_PURPOSE_NUMBER</property>
<property name="xalign">0.5</property>
<property name="valign">center</property>
<property name="placeholder-text">4242</property>
<property name="width-chars">5</property>
</object>
</child>
</object>
</child>
<!-- position -->
<child>
<object class="AdwComboRow" id="position">
<property name="title" translatable="yes">position</property>
<property name="model">
<object class="GtkStringList">
<items>
<item>Left</item>
<item>Right</item>
<item>Top</item>
<item>Bottom</item>
</items>
</object>
</property>
</object>
</child>
<!-- delete button -->
<child>
<object class="AdwActionRow" id="delete_row">
<property name="title">delete this client</property>
<child>
<object class="GtkButton" id="delete_button">
<signal name="activate" handler="handle_client_delete" object="delete_row" swapped="true"/>
<property name="icon-name">user-trash-symbolic</property>
<property name="valign">center</property>
<property name="halign">center</property>
<property name="name">delete-button</property>
</object>
</child>
</object>
</child>
</template>
</interface>

171
resources/mouse-icon.svg Normal file
View File

@@ -0,0 +1,171 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="48"
height="48"
viewBox="0 0 12.7 12.7"
version="1.1"
id="svg1"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
sodipodi:docname="mouse-icon.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="22.737887"
inkscape:cx="19.54887"
inkscape:cy="26.167778"
inkscape:window-width="2560"
inkscape:window-height="1374"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g20"
transform="translate(1.1586889,0.39019296)">
<g
id="g8"
transform="translate(-0.11519282,-3.9659242)">
<g
id="g6"
transform="translate(0.67275315,0.39959697)">
<g
id="g5">
<rect
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
id="rect4"
width="1.3032579"
height="1.3032579"
x="1.7199994"
y="7.5408325"
ry="0.3373504" />
<rect
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
id="rect4-2"
width="1.3032579"
height="1.3032579"
x="3.8428385"
y="7.5408325"
ry="0.3373504" />
</g>
<rect
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
id="rect4-3"
width="1.3032579"
height="1.3032579"
x="2.781419"
y="5.1382394"
ry="0.3373504" />
</g>
<path
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
d="M 1.1519282,7.3907619 H 7.059674"
id="path5" />
<path
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
d="M 4.1058009,6.8410941 V 7.3907617"
id="path6" />
<path
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
d="m 5.1672204,7.9404294 2e-7,-0.5496677"
id="path7" />
<path
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
d="M 3.0443815,7.9404294 V 7.3907617"
id="path8" />
</g>
<path
style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
d="M 6.9444811,3.4248375 Z"
id="path9" />
<path
style="color:#000000;fill:#000000;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="m 6.9840449,3.4464199 c -0.072714,-0.0035 -0.1209639,-0.2113583 -0.125,-0.1386718 -0.0035,0.072714 0.052314,0.1346357 0.125,0.1386718 0,0 0.6614057,0.034643 1.3535156,0.4765625 0.6921097,0.4419191 1.4111567,1.2803292 1.5136717,2.9433594 0.05132,0.832563 -0.07521,1.3855916 -0.279297,1.75 -0.20409,0.3644084 -0.482943,0.5482749 -0.777343,0.640625 -0.5888014,0.1847002 -1.2265629,-0.021484 -1.2265629,-0.021484 -0.069024,-0.023541 -0.144095,0.013122 -0.1679688,0.082031 -0.023366,0.069587 0.014295,0.1449093 0.083984,0.1679687 0,0 0.6961634,0.2406696 1.3886717,0.023437 C 9.2189712,9.4003039 9.5672292,9.1706004 9.8043572,8.7472012 10.041486,8.323802 10.170261,7.7150888 10.116858,6.8487637 10.009921,5.1140179 9.2320232,4.3532014 8.4801387,3.8731154 7.7282538,3.3930294 6.9840449,3.4464198 6.9840449,3.4464199 Z"
id="path18"
sodipodi:nodetypes="cccsssscccssssc" />
<g
id="g19"
transform="matrix(1.8148709,0,0,1.8148709,-4.1533763,-7.8818885)">
<g
id="g17"
transform="translate(0.01163623,0.23038484)">
<ellipse
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
id="path10"
cx="3.9823804"
cy="8.17869"
rx="0.49368349"
ry="0.62533247" />
<ellipse
style="fill:#3d3d3d;fill-opacity:1;stroke:none;stroke-width:0.168876;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
id="ellipse17"
cx="3.9823804"
cy="8.17869"
rx="0.31096464"
ry="0.40317491" />
</g>
<path
id="path11"
style="stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round"
d="M 7.479305,9.4704944 C 7.4964603,9.9336885 6.9306558,9.9678313 5.3811502,10.087599 3.2109768,10.255341 2.4751992,9.6707727 2.4355055,9.5280908 2.3112754,9.0815374 3.8270232,8.4090748 5.3811502,8.4090748 c 1.5633309,0 2.0816988,0.6171052 2.0981548,1.0614196 z"
sodipodi:nodetypes="sssss" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
id="path12"
cx="3.5281858"
cy="9.0632057"
r="0.18513133" />
<g
id="g18"
transform="translate(0.01163623,0.23038484)">
<ellipse
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
id="path10-2"
cx="4.6085634"
cy="8.17869"
rx="0.49368349"
ry="0.62533247" />
<ellipse
style="fill:#3d3d3d;fill-opacity:1;stroke:none;stroke-width:0.168876;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
id="ellipse16"
cx="4.6085634"
cy="8.17869"
rx="0.31096464"
ry="0.40317491" />
</g>
<ellipse
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.112226;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
id="circle18"
cx="3.5003331"
cy="9.0344076"
rx="0.078639306"
ry="0.07816644" />
<ellipse
style="fill:#4f4f4f;fill-opacity:1;stroke:none;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="path19"
cx="2.4818404"
cy="9.4499254"
rx="0.05348238"
ry="0.11930636" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/de/feschber/LanMouse">
<file compressed="true" preprocess="xml-stripblanks">window.ui</file>
<file compressed="true" preprocess="xml-stripblanks">client_row.ui</file>
<file compressed="true">style.css</file>
<file compressed="true">style-dark.css</file>
</gresource>
<gresource prefix="/de/feschber/LanMouse/icons">
<file compressed="true" preprocess="xml-stripblanks">mouse-icon.svg</file>
</gresource>
</gresources>

11
resources/style-dark.css Normal file
View File

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

11
resources/style.css Normal file
View File

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

146
resources/window.ui Normal file
View File

@@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<requires lib="libadwaita" version="1.0"/>
<menu id="main-menu">
<item>
<attribute name="label" translatable="yes">_Close window</attribute>
<attribute name="action">window.close</attribute>
</item>
</menu>
<template class="LanMouseWindow" parent="AdwApplicationWindow">
<property name="width-request">600</property>
<property name="height-request">700</property>
<property name="title" translatable="yes">Lan Mouse</property>
<property name="show-menubar">True</property>
<property name="content">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child type="top">
<object class="AdwHeaderBar">
<child type ="end">
<object class="GtkMenuButton">
<property name="icon-name">open-menu-symbolic</property>
<property name="menu-model">main-menu</property>
</object>
</child>
<style>
<class name="flat"/>
</style>
</object>
</child>
<child>
<object class="AdwToastOverlay" id="toast_overlay">
<child>
<object class="AdwStatusPage">
<property name="title" translatable="yes">Lan Mouse</property>
<property name="description" translatable="yes">easily use your mouse and keyboard on multiple computers</property>
<property name="icon-name">mouse-icon</property>
<property name="child">
<object class="AdwClamp">
<property name="maximum-size">600</property>
<property name="tightening-threshold">0</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="AdwPreferencesGroup">
<property name="title" translatable="yes">General</property>
<!--
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">enable</property>
<child type="suffix">
<object class="GtkSwitch">
<property name="valign">center</property>
<property name="tooltip-text" translatable="yes">enable</property>
</object>
</child>
</object>
</child>
-->
<child>
<object class="AdwActionRow">
<property name="title">port</property>
<child>
<object class="GtkEntry" id="port_entry">
<signal name="activate" handler="handle_port_edit_apply" swapped="true"/>
<signal name="changed" handler="handle_port_changed" swapped="true"/>
<!-- <signal name="delete-text" handler="handle_port_changed" swapped="true"/> -->
<!-- <property name="title" translatable="yes">port</property> -->
<property name="placeholder-text">4242</property>
<property name="width-chars">5</property>
<property name="xalign">0.5</property>
<property name="valign">center</property>
<!-- <property name="show-apply-button">True</property> -->
<property name="input-purpose">GTK_INPUT_PURPOSE_DIGITS</property>
</object>
</child>
<child>
<object class="GtkButton" id="port_edit_apply">
<signal name="clicked" handler="handle_port_edit_apply" swapped="true"/>
<property name="icon-name">object-select-symbolic</property>
<property name="valign">center</property>
<property name="visible">false</property>
<property name="name">port-edit-apply</property>
</object>
</child>
<child>
<object class="GtkButton" id="port_edit_cancel">
<signal name="clicked" handler="handle_port_edit_cancel" swapped="true"/>
<property name="icon-name">process-stop-symbolic</property>
<property name="valign">center</property>
<property name="visible">false</property>
<property name="name">port-edit-cancel</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<property name="title" translatable="yes">Connections</property>
<property name="header-suffix">
<object class="GtkButton">
<signal name="clicked" handler="handle_add_client_pressed" swapped="true"/>
<property name="child">
<object class="AdwButtonContent">
<property name="icon-name">list-add-symbolic</property>
<property name="label" translatable="yes">Add</property>
</object>
</property>
<style>
<class name="flat"/>
</style>
</object>
</property>
<child>
<object class="GtkListBox" id="client_list">
<property name="selection-mode">none</property>
<child type="placeholder">
<object class="AdwActionRow" id="client_placeholder">
<property name="title">No connections!</property>
<property name="subtitle">add a new client via the + button</property>
</object>
</child>
<style>
<class name="boxed-list" />
</style>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</property>
</object>
</child>
</object>
</child>
</object>
</property>
</template>
</interface>

View File

@@ -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;

View 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) {}
}

View File

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

View File

@@ -0,0 +1,238 @@
use crate::client::{ClientEvent, ClientHandle};
use crate::consumer::EventConsumer;
use crate::event::{Event, KeyboardEvent, PointerEvent};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use core_graphics::display::{CGDisplayBounds, CGMainDisplayID, CGPoint};
use core_graphics::event::{
CGEvent, CGEventTapLocation, CGEventType, CGMouseButton, EventField, ScrollEventUnit,
};
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
use std::ops::{Index, IndexMut};
pub struct MacOSConsumer {
pub event_source: CGEventSource,
button_state: ButtonState,
}
struct ButtonState {
left: bool,
right: bool,
center: bool,
}
impl Index<CGMouseButton> for ButtonState {
type Output = bool;
fn index(&self, index: CGMouseButton) -> &Self::Output {
match index {
CGMouseButton::Left => &self.left,
CGMouseButton::Right => &self.right,
CGMouseButton::Center => &self.center,
}
}
}
impl IndexMut<CGMouseButton> for ButtonState {
fn index_mut(&mut self, index: CGMouseButton) -> &mut Self::Output {
match index {
CGMouseButton::Left => &mut self.left,
CGMouseButton::Right => &mut self.right,
CGMouseButton::Center => &mut self.center,
}
}
}
unsafe impl Send for MacOSConsumer {}
impl MacOSConsumer {
pub fn new() -> Result<Self> {
let event_source = match CGEventSource::new(CGEventSourceStateID::CombinedSessionState) {
Ok(e) => e,
Err(_) => return Err(anyhow!("event source creation failed!")),
};
let button_state = ButtonState {
left: false,
right: false,
center: false,
};
Ok(Self {
event_source,
button_state,
})
}
fn get_mouse_location(&self) -> Option<CGPoint> {
let event: CGEvent = CGEvent::new(self.event_source.clone()).ok()?;
Some(event.location())
}
}
#[async_trait]
impl EventConsumer for MacOSConsumer {
async fn consume(&mut self, event: Event, _client_handle: ClientHandle) {
match event {
Event::Pointer(pointer_event) => match pointer_event {
PointerEvent::Motion {
time: _,
relative_x,
relative_y,
} => {
// FIXME secondary displays?
let (min_x, min_y, max_x, max_y) = unsafe {
let display = CGMainDisplayID();
let bounds = CGDisplayBounds(display);
let min_x = bounds.origin.x;
let max_x = bounds.origin.x + bounds.size.width;
let min_y = bounds.origin.y;
let max_y = bounds.origin.y + bounds.size.height;
(min_x as f64, min_y as f64, max_x as f64, max_y as f64)
};
let mut mouse_location = match self.get_mouse_location() {
Some(l) => l,
None => {
log::warn!("could not get mouse location!");
return;
}
};
mouse_location.x = (mouse_location.x + relative_x).clamp(min_x, max_x - 1.);
mouse_location.y = (mouse_location.y + relative_y).clamp(min_y, max_y - 1.);
let mut event_type = CGEventType::MouseMoved;
if self.button_state.left {
event_type = CGEventType::LeftMouseDragged
} else if self.button_state.right {
event_type = CGEventType::RightMouseDragged
} else if self.button_state.center {
event_type = CGEventType::OtherMouseDragged
};
let event = match CGEvent::new_mouse_event(
self.event_source.clone(),
event_type,
mouse_location,
CGMouseButton::Left,
) {
Ok(e) => e,
Err(_) => {
log::warn!("mouse event creation failed!");
return;
}
};
event.set_integer_value_field(
EventField::MOUSE_EVENT_DELTA_X,
relative_x as i64,
);
event.set_integer_value_field(
EventField::MOUSE_EVENT_DELTA_Y,
relative_y as i64,
);
event.post(CGEventTapLocation::HID);
}
PointerEvent::Button {
time: _,
button,
state,
} => {
let (event_type, mouse_button) = match (button, state) {
(b, 1) if b == crate::event::BTN_LEFT => {
(CGEventType::LeftMouseDown, CGMouseButton::Left)
}
(b, 0) if b == crate::event::BTN_LEFT => {
(CGEventType::LeftMouseUp, CGMouseButton::Left)
}
(b, 1) if b == crate::event::BTN_RIGHT => {
(CGEventType::RightMouseDown, CGMouseButton::Right)
}
(b, 0) if b == crate::event::BTN_RIGHT => {
(CGEventType::RightMouseUp, CGMouseButton::Right)
}
(b, 1) if b == crate::event::BTN_MIDDLE => {
(CGEventType::OtherMouseDown, CGMouseButton::Center)
}
(b, 0) if b == crate::event::BTN_MIDDLE => {
(CGEventType::OtherMouseUp, CGMouseButton::Center)
}
_ => {
log::warn!("invalid button event: {button},{state}");
return;
}
};
// store button state
self.button_state[mouse_button] = state == 1;
let location = self.get_mouse_location().unwrap();
let event = match CGEvent::new_mouse_event(
self.event_source.clone(),
event_type,
location,
mouse_button,
) {
Ok(e) => e,
Err(()) => {
log::warn!("mouse event creation failed!");
return;
}
};
event.post(CGEventTapLocation::HID);
}
PointerEvent::Axis {
time: _,
axis,
value,
} => {
let value = value as i32 / 10; // FIXME: high precision scroll events
let (count, wheel1, wheel2, wheel3) = match axis {
0 => (1, value, 0, 0), // 0 = vertical => 1 scroll wheel device (y axis)
1 => (2, 0, value, 0), // 1 = horizontal => 2 scroll wheel devices (y, x) -> (0, x)
_ => {
log::warn!("invalid scroll event: {axis}, {value}");
return;
}
};
let event = match CGEvent::new_scroll_event(
self.event_source.clone(),
ScrollEventUnit::LINE,
count,
wheel1,
wheel2,
wheel3,
) {
Ok(e) => e,
Err(()) => {
log::warn!("scroll event creation failed!");
return;
}
};
event.post(CGEventTapLocation::HID);
}
PointerEvent::Frame { .. } => {}
},
Event::Keyboard(keyboard_event) => match keyboard_event {
KeyboardEvent::Key { .. } => {
/*
let code = CGKeyCode::from_le(key as u16);
let event = match CGEvent::new_keyboard_event(
self.event_source.clone(),
code,
match state { 1 => true, _ => false }
) {
Ok(e) => e,
Err(_) => {
log::warn!("unable to create key event");
return
}
};
event.post(CGEventTapLocation::HID);
*/
}
KeyboardEvent::Modifiers { .. } => {}
},
_ => (),
}
}
async fn notify(&mut self, _client_event: ClientEvent) {}
async fn destroy(&mut self) {}
}

View File

@@ -1,25 +1,77 @@
use std::sync::mpsc::Receiver;
use crate::event::{KeyboardEvent, PointerEvent};
use crate::{
consumer::EventConsumer,
event::{KeyboardEvent, PointerEvent},
scancode,
};
use anyhow::Result;
use async_trait::async_trait;
use winapi::um::winuser::{SendInput, KEYEVENTF_EXTENDEDKEY};
use winapi::{
self,
um::winuser::{INPUT, INPUT_MOUSE, LPINPUT, MOUSEEVENTF_MOVE, MOUSEINPUT,
MOUSEEVENTF_LEFTDOWN,
MOUSEEVENTF_RIGHTDOWN,
MOUSEEVENTF_MIDDLEDOWN,
MOUSEEVENTF_LEFTUP,
MOUSEEVENTF_RIGHTUP,
MOUSEEVENTF_MIDDLEUP,
MOUSEEVENTF_WHEEL,
MOUSEEVENTF_HWHEEL, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_SCANCODE, KEYEVENTF_KEYUP,
um::winuser::{
INPUT, INPUT_KEYBOARD, INPUT_MOUSE, KEYBDINPUT, KEYEVENTF_KEYUP, KEYEVENTF_SCANCODE,
LPINPUT, MOUSEEVENTF_HWHEEL, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP,
MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN,
MOUSEEVENTF_RIGHTUP, MOUSEEVENTF_WHEEL, MOUSEINPUT,
},
};
use crate::{
client::{Client, ClientHandle},
client::{ClientEvent, ClientHandle},
event::Event,
};
pub struct WindowsConsumer {}
impl WindowsConsumer {
pub fn new() -> Result<Self> {
Ok(Self {})
}
}
#[async_trait]
impl EventConsumer for WindowsConsumer {
async fn consume(&mut self, event: Event, _: ClientHandle) {
match event {
Event::Pointer(pointer_event) => match pointer_event {
PointerEvent::Motion {
time: _,
relative_x,
relative_y,
} => {
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::Frame {} => {}
},
Event::Keyboard(keyboard_event) => match keyboard_event {
KeyboardEvent::Key {
time: _,
key,
state,
} => key_event(key, state),
KeyboardEvent::Modifiers { .. } => {}
},
_ => {}
}
}
async fn notify(&mut self, _: ClientEvent) {
// nothing to do
}
async fn destroy(&mut self) {}
}
fn send_mouse_input(mi: MOUSEINPUT) {
unsafe {
let mut input = INPUT {
@@ -27,8 +79,8 @@ fn send_mouse_input(mi: MOUSEINPUT) {
u: std::mem::transmute(mi),
};
winapi::um::winuser::SendInput(
1 as u32,
SendInput(
1_u32,
&mut input as LPINPUT,
std::mem::size_of::<INPUT>() as i32,
);
@@ -53,18 +105,19 @@ fn mouse_button(button: u32, state: u32) {
0x110 => MOUSEEVENTF_LEFTUP,
0x111 => MOUSEEVENTF_RIGHTUP,
0x112 => MOUSEEVENTF_MIDDLEUP,
_ => return
}
_ => return,
},
1 => match button {
0x110 => MOUSEEVENTF_LEFTDOWN,
0x111 => MOUSEEVENTF_RIGHTDOWN,
0x112 => MOUSEEVENTF_MIDDLEDOWN,
_ => return
}
_ => return
_ => return,
},
_ => return,
};
let mi = MOUSEINPUT {
dx: 0, dy: 0, // no movement
dx: 0,
dy: 0, // no movement
mouseData: 0,
dwFlags: dw_flags,
time: 0,
@@ -77,10 +130,11 @@ fn scroll(axis: u8, value: f64) {
let event_type = match axis {
0 => MOUSEEVENTF_WHEEL,
1 => MOUSEEVENTF_HWHEEL,
_ => return
_ => return,
};
let mi = MOUSEINPUT {
dx: 0, dy: 0,
dx: 0,
dy: 0,
mouseData: (-value * 15.0) as i32 as u32,
dwFlags: event_type,
time: 0,
@@ -90,14 +144,22 @@ fn scroll(axis: u8, value: f64) {
}
fn key_event(key: u32, state: u8) {
let scancode = match linux_keycode_to_windows_scancode(key) {
Some(code) => code,
None => return,
};
let extended = scancode > 0xff;
let scancode = scancode & 0xff;
let ki = KEYBDINPUT {
wVk: 0,
wScan: key as u16,
dwFlags: KEYEVENTF_SCANCODE | match state {
0 => KEYEVENTF_KEYUP,
1 => 0u32,
_ => return
},
wScan: scancode,
dwFlags: KEYEVENTF_SCANCODE
| if extended { KEYEVENTF_EXTENDEDKEY } else { 0 }
| match state {
0 => KEYEVENTF_KEYUP,
1 => 0u32,
_ => return,
},
time: 0,
dwExtraInfo: 0,
};
@@ -111,30 +173,26 @@ fn send_keyboard_input(ki: KEYBDINPUT) {
u: std::mem::zeroed(),
};
*input.u.ki_mut() = ki;
winapi::um::winuser::SendInput(1 as u32, &mut input, std::mem::size_of::<INPUT>() as i32);
SendInput(1_u32, &mut input, std::mem::size_of::<INPUT>() as i32);
}
}
pub fn run(event_rx: Receiver<(Event, ClientHandle)>, _clients: Vec<Client>) {
loop {
match event_rx.recv().expect("event receiver unavailable").0 {
Event::Pointer(pointer_event) => match pointer_event {
PointerEvent::Motion {
time: _,
relative_x,
relative_y,
} => {
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::Frame {} => {}
},
Event::Keyboard(keyboard_event) => match keyboard_event {
KeyboardEvent::Key { time:_, key, state } => { key_event(key, state) }
KeyboardEvent::Modifiers { .. } => {}
},
Event::Release() => {}
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)
}

View File

@@ -1,16 +1,16 @@
use crate::client::{Client, ClientHandle};
use crate::request::{self, Request};
use crate::client::{ClientEvent, ClientHandle};
use crate::consumer::EventConsumer;
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::mpsc::Receiver;
use std::time::Duration;
use std::{io, thread};
use std::{
io::{BufWriter, Write},
os::unix::prelude::AsRawFd,
};
use std::io;
use std::os::fd::{AsFd, OwnedFd};
use wayland_client::backend::WaylandError;
use wayland_client::WEnum;
use wayland_client::globals::BindError;
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,
zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1 as Vp,
@@ -21,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},
@@ -30,267 +28,233 @@ use wayland_client::{
Connection, Dispatch, EventQueue, QueueHandle,
};
use tempfile;
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,
qh: QueueHandle<Self>,
vpm: VpManager,
vkm: VkManager,
}
// App State, implements Dispatch event handlers
struct App {
input_for_client: HashMap<ClientHandle, VirtualInput>,
seat: wl_seat::WlSeat,
event_rx: Receiver<(Event, ClientHandle)>,
virtual_input_manager: VirtualInputManager,
queue: EventQueue<Self>,
qh: QueueHandle<Self>,
pub(crate) struct WlrootsConsumer {
last_flush_failed: bool,
state: State,
queue: EventQueue<State>,
}
pub fn run(event_rx: Receiver<(Event, ClientHandle)>, clients: Vec<Client>) {
let mut app = App::new(event_rx, clients);
app.run();
}
impl App {
pub fn new(event_rx: Receiver<(Event, ClientHandle)>, clients: Vec<Client>) -> Self {
let conn = Connection::connect_to_env().unwrap();
let (globals, queue) = registry_queue_init::<App>(&conn).unwrap();
impl WlrootsConsumer {
pub fn new() -> Result<Self> {
let conn = Connection::connect_to_env()?;
let (globals, queue) = registry_queue_init::<State>(&conn)?;
let qh = queue.handle();
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)) => {
eprintln!("zwlr_virtual_pointer_v1: {e1}");
eprintln!("zwp_virtual_keyboard_v1: {e2}");
eprintln!("org_kde_kwin_fake_input: {e3}");
panic!("neither wlroots nor kde input emulation protocol supported!")
}
_ => {
panic!()
}
let seat: wl_seat::WlSeat = match globals.bind(&qh, 7..=8, ()) {
Ok(wl_seat) => wl_seat,
Err(_) => return Err(anyhow!("wl_seat >= v7 not supported")),
};
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 seat: wl_seat::WlSeat = globals.bind(&qh, 7..=8, ()).unwrap();
let mut app = App {
input_for_client,
seat,
event_rx,
virtual_input_manager,
let mut consumer = WlrootsConsumer {
last_flush_failed: false,
state: State {
keymap: None,
input_for_client,
seat,
vpm,
vkm,
qh,
},
queue,
qh,
};
for client in clients {
app.add_client(client);
}
app
}
pub fn run(&mut self) {
loop {
let (event, client) = self.event_rx.recv().expect("event receiver unavailable");
if let Some(virtual_input) = self.input_for_client.get(&client) {
virtual_input.consume_event(event).unwrap();
if let Err(e) = self.queue.flush() {
eprintln!("{}", e);
}
}
}
}
fn add_client(&mut self, client: Client) {
// 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, ());
// receive keymap from device
eprint!("\rconnecting to {} ", client.addr);
let mut attempts = 0;
let data = loop {
let result = request::request_data(client.addr, Request::KeyMap);
eprint!("\rconnecting to {} ", client.addr);
for _ in 0..attempts {
eprint!(".");
}
match result {
Ok(data) => break data,
Err(e) => {
eprint!(" - {}", e);
}
}
io::stderr().flush().unwrap();
thread::sleep(Duration::from_millis(500));
attempts += 1;
};
eprint!("\rconnecting to {} ", client.addr);
for _ in 0..attempts {
eprint!(".");
}
eprintln!(" done! ");
// TODO use shm_open
let f = tempfile::tempfile().unwrap();
let mut buf = BufWriter::new(&f);
buf.write_all(&data[..]).unwrap();
buf.flush().unwrap();
keyboard.keymap(1, f.as_raw_fd(), data.len() as u32);
let vinput = VirtualInput::Wlroots { pointer, keyboard };
self.input_for_client.insert(client.handle, vinput);
}
VirtualInputManager::Kde { fake_input } => {
let fake_input = fake_input.clone();
let vinput = VirtualInput::Kde { fake_input };
self.input_for_client.insert(client.handle, vinput);
}
while consumer.state.keymap.is_none() {
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() };
// log::debug!("{:?}", &mmap[..100]);
Ok(consumer)
}
}
enum VirtualInput {
Wlroots { pointer: Vp, keyboard: Vk },
Kde { fake_input: OrgKdeKwinFakeInput },
impl State {
fn add_client(&mut self, client: ClientHandle) {
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_fd(), *size);
} else {
panic!("no keymap");
}
let vinput = VirtualInput { pointer, keyboard };
self.input_for_client.insert(client, vinput);
}
}
#[async_trait]
impl EventConsumer for WlrootsConsumer {
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();
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;
}
}
}
}
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() {
log::error!("{}", e);
}
}
}
async fn destroy(&mut self) {}
}
struct VirtualInput {
pointer: Vp,
keyboard: Vk,
}
impl VirtualInput {
fn consume_event(&self, event: Event) -> Result<(),()> {
fn consume_event(&self, event: Event) -> Result<(), ()> {
match event {
Event::Pointer(e) => match e {
PointerEvent::Motion {
time,
relative_x,
relative_y,
} => match self {
VirtualInput::Wlroots {
pointer,
keyboard: _,
Event::Pointer(e) => {
match e {
PointerEvent::Motion {
time,
relative_x,
relative_y,
} => self.pointer.motion(time, relative_x, relative_y),
PointerEvent::Button {
time,
button,
state,
} => {
pointer.motion(time, relative_x, relative_y);
pointer.frame();
let state: ButtonState = state.try_into()?;
self.pointer.button(time, button, state);
}
VirtualInput::Kde { fake_input } => {
fake_input.pointer_motion(relative_y, relative_y);
}
},
PointerEvent::Button {
time,
button,
state,
} => {
let state: ButtonState = state.try_into()?;
match self {
VirtualInput::Wlroots {
pointer,
keyboard: _,
} => {
pointer.button(time, button, state);
pointer.frame();
}
VirtualInput::Kde { fake_input } => {
fake_input.button(button, state as u32);
}
PointerEvent::Axis { time, axis, value } => {
let axis: Axis = (axis as u32).try_into()?;
self.pointer.axis(time, axis, value);
self.pointer.frame();
}
PointerEvent::Frame {} => self.pointer.frame(),
}
PointerEvent::Axis { time, axis, value } => {
let axis: Axis = (axis as u32).try_into()?;
match self {
VirtualInput::Wlroots {
pointer,
keyboard: _,
} => {
pointer.axis(time, axis, value);
pointer.frame();
}
VirtualInput::Kde { fake_input } => {
fake_input.axis(axis as u32, value);
}
}
}
PointerEvent::Frame {} => match self {
VirtualInput::Wlroots {
pointer,
keyboard: _,
} => {
pointer.frame();
}
VirtualInput::Kde { fake_input: _ } => {}
},
},
self.pointer.frame();
}
Event::Keyboard(e) => match e {
KeyboardEvent::Key { time, key, state } => match self {
VirtualInput::Wlroots {
pointer: _,
keyboard,
} => {
keyboard.key(time, key, state as u32);
}
VirtualInput::Kde { fake_input } => {
fake_input.keyboard_key(key, state as u32);
}
},
KeyboardEvent::Key { time, key, state } => {
self.keyboard.key(time, key, state as u32);
}
KeyboardEvent::Modifiers {
mods_depressed,
mods_latched,
mods_locked,
group,
} => match self {
VirtualInput::Wlroots {
pointer: _,
keyboard,
} => {
keyboard.modifiers(mods_depressed, mods_latched, mods_locked, group);
}
VirtualInput::Kde { fake_input: _ } => {}
},
},
Event::Release() => match self {
VirtualInput::Wlroots {
pointer: _,
keyboard,
} => {
keyboard.modifiers(77, 0, 0, 0);
keyboard.modifiers(0, 0, 0, 0);
self.keyboard
.modifiers(mods_depressed, mods_latched, mods_locked, group);
}
VirtualInput::Kde { fake_input: _ } => {}
},
_ => {}
}
Ok(())
}
}
delegate_noop!(App: Vp);
delegate_noop!(App: Vk);
delegate_noop!(App: VpManager);
delegate_noop!(App: VkManager);
delegate_noop!(App: wl_seat::WlSeat);
delegate_noop!(App: OrgKdeKwinFakeInput);
delegate_noop!(State: Vp);
delegate_noop!(State: Vk);
delegate_noop!(State: VpManager);
delegate_noop!(State: VkManager);
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for App {
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for State {
fn event(
_: &mut App,
_: &mut State,
_: &wl_registry::WlRegistry,
_: wl_registry::Event,
_: &GlobalListContents,
_: &Connection,
_: &QueueHandle<App>,
_: &QueueHandle<State>,
) {
}
}
impl Dispatch<WlKeyboard, ()> for State {
fn event(
state: &mut Self,
_: &WlKeyboard,
event: <WlKeyboard as wayland_client::Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
if let wl_keyboard::Event::Keymap { format, fd, size } = event {
state.keymap = Some((u32::from(format), fd, size));
}
}
}
impl Dispatch<WlSeat, ()> for State {
fn event(
_: &mut Self,
seat: &WlSeat,
event: <WlSeat as wayland_client::Proxy>::Event,
_: &(),
_: &Connection,
qhandle: &QueueHandle<Self>,
) {
if let wl_seat::Event::Capabilities {
capabilities: WEnum::Value(capabilities),
} = event
{
if capabilities.contains(wl_seat::Capability::Keyboard) {
seat.get_keyboard(qhandle, ());
}
}
}
}

View File

@@ -1,49 +1,149 @@
use std::{ptr, sync::mpsc::Receiver};
use x11::{xlib, xtest};
use crate::{
client::{Client, ClientHandle},
event::Event,
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use std::ptr;
use x11::{
xlib::{self, XCloseDisplay},
xtest,
};
fn open_display() -> Option<*mut xlib::Display> {
unsafe {
match xlib::XOpenDisplay(ptr::null()) {
d if d == ptr::null::<xlib::Display>() as *mut xlib::Display => None,
display => Some(display),
use crate::{
client::ClientHandle,
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() -> Result<Self> {
let display = unsafe {
match xlib::XOpenDisplay(ptr::null()) {
d if d == ptr::null::<xlib::Display>() as *mut xlib::Display => {
Err(anyhow!("could not open display"))
}
display => Ok(display),
}
}?;
Ok(Self { display })
}
fn relative_motion(&self, dx: i32, dy: i32) {
unsafe {
xtest::XTestFakeRelativeMotionEvent(self.display, dx, dy, 0, 0);
}
}
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);
}
}
}
fn relative_motion(display: *mut xlib::Display, dx: i32, dy: i32) {
unsafe {
xtest::XTestFakeRelativeMotionEvent(display, dx, dy, 0, 0);
xlib::XFlush(display);
impl Drop for X11Consumer {
fn drop(&mut self) {
unsafe {
XCloseDisplay(self.display);
}
}
}
pub fn run(event_rx: Receiver<(Event, ClientHandle)>, _clients: Vec<Client>) {
let display = match open_display() {
None => panic!("could not open display!"),
Some(display) => display,
};
loop {
match event_rx.recv().expect("event receiver unavailable").0 {
#[async_trait]
impl EventConsumer for X11Consumer {
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,
} => {
relative_motion(display, relative_x as i32, relative_y as i32);
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::Release() => {}
Event::Keyboard(KeyboardEvent::Key {
time: _,
key,
state,
}) => {
self.emulate_key(key, state);
}
_ => {}
}
unsafe {
xlib::XFlush(self.display);
}
}
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) {}
}

View File

@@ -1,9 +1,139 @@
use std::sync::mpsc::Receiver;
use anyhow::Result;
use ashpd::{
desktop::{
remote_desktop::{Axis, DeviceType, KeyState, RemoteDesktop},
Session,
},
WindowIdentifier,
};
use async_trait::async_trait;
use crate::{event::Event, client::{ClientHandle, Client}};
use crate::{
client::ClientEvent,
consumer::EventConsumer,
event::{
Event::{Keyboard, Pointer},
KeyboardEvent, PointerEvent,
},
};
pub(crate) fn run(_consume_rx: Receiver<(Event, ClientHandle)>, _clients: Vec<Client>) {
todo!()
pub struct DesktopPortalConsumer<'a> {
proxy: RemoteDesktop<'a>,
session: Session<'a>,
}
impl<'a> DesktopPortalConsumer<'a> {
pub async fn new() -> Result<DesktopPortalConsumer<'a>> {
log::debug!("connecting to org.freedesktop.portal.RemoteDesktop portal ...");
let proxy = RemoteDesktop::new().await?;
log::debug!("creating session ...");
let session = proxy.create_session().await?;
log::debug!("selecting devices ...");
proxy
.select_devices(&session, DeviceType::Keyboard | DeviceType::Pointer)
.await?;
let _ = proxy
.start(&session, &WindowIdentifier::default())
.await?
.response()?;
log::debug!("started session");
Ok(Self { proxy, session })
}
}
#[async_trait]
impl<'a> EventConsumer for DesktopPortalConsumer<'a> {
async fn consume(&mut self, event: crate::event::Event, _client: crate::client::ClientHandle) {
match event {
Pointer(p) => {
match p {
PointerEvent::Motion {
time: _,
relative_x,
relative_y,
} => {
if let Err(e) = self
.proxy
.notify_pointer_motion(&self.session, relative_x, relative_y)
.await
{
log::warn!("{e}");
}
}
PointerEvent::Button {
time: _,
button,
state,
} => {
let state = match state {
0 => KeyState::Released,
_ => KeyState::Pressed,
};
if let Err(e) = self
.proxy
.notify_pointer_button(&self.session, button as i32, state)
.await
{
log::warn!("{e}");
}
}
PointerEvent::Axis {
time: _,
axis,
value,
} => {
let axis = match axis {
0 => Axis::Vertical,
_ => Axis::Horizontal,
};
// TODO smooth scrolling
if let Err(e) = self
.proxy
.notify_pointer_axis_discrete(&self.session, axis, value as i32)
.await
{
log::warn!("{e}");
}
}
PointerEvent::Frame {} => {}
}
}
Keyboard(k) => {
match k {
KeyboardEvent::Key {
time: _,
key,
state,
} => {
let state = match state {
0 => KeyState::Released,
_ => KeyState::Pressed,
};
if let Err(e) = self
.proxy
.notify_keyboard_keycode(&self.session, key as i32, state)
.await
{
log::warn!("{e}");
}
}
KeyboardEvent::Modifiers { .. } => {
// ignore
}
}
}
_ => {}
}
}
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}");
}
}
}

View File

@@ -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;

View File

@@ -0,0 +1,38 @@
use std::io;
use std::pin::Pin;
use std::task::{Context, Poll};
use futures_core::Stream;
use crate::event::Event;
use crate::producer::EventProducer;
use crate::client::{ClientEvent, ClientHandle};
pub struct DummyProducer {}
impl DummyProducer {
pub fn new() -> Self {
Self {}
}
}
impl Default for DummyProducer {
fn default() -> Self {
Self::new()
}
}
impl EventProducer for DummyProducer {
fn notify(&mut self, _: ClientEvent) {}
fn release(&mut self) {}
}
impl Stream for DummyProducer {
type Item = io::Result<(ClientHandle, Event)>;
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Poll::Pending
}
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,31 @@
use std::sync::mpsc::SyncSender;
use anyhow::{anyhow, Result};
use core::task::{Context, Poll};
use futures::Stream;
use std::{io, pin::Pin};
use crate::{
client::{Client, ClientHandle},
client::{ClientEvent, ClientHandle},
event::Event,
request::Server,
producer::EventProducer,
};
pub fn run(_produce_tx: SyncSender<(Event, ClientHandle)>, _server: Server, _clients: Vec<Client>) {
todo!();
pub struct WindowsProducer {}
impl EventProducer for WindowsProducer {
fn notify(&mut self, _: ClientEvent) {}
fn release(&mut self) {}
}
impl WindowsProducer {
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
}
}

View File

@@ -1,9 +1,35 @@
use std::sync::mpsc::SyncSender;
use anyhow::{anyhow, Result};
use std::io;
use std::task::Poll;
use futures_core::Stream;
use crate::client::Client;
use crate::event::Event;
use crate::request::Server;
use crate::producer::EventProducer;
pub fn run(_produce_tx: SyncSender<(Event, u32)>, _request_server: Server, _clients: Vec<Client>) {
todo!()
use crate::client::{ClientEvent, ClientHandle};
pub struct X11Producer {}
impl X11Producer {
pub fn new() -> Result<Self> {
Err(anyhow!("not implemented"))
}
}
impl EventProducer for X11Producer {
fn notify(&mut self, _: ClientEvent) {}
fn release(&mut self) {}
}
impl Stream for X11Producer {
type Item = io::Result<(ClientHandle, Event)>;
fn poll_next(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
Poll::Pending
}
}

View File

@@ -1,8 +1,13 @@
use std::{net::SocketAddr, error::Error, fmt::Display, sync::{Arc, atomic::{AtomicBool, Ordering, AtomicU32}, RwLock}};
use std::{
collections::HashSet,
fmt::Display,
net::{IpAddr, SocketAddr},
time::Instant,
};
use crate::{config::{self, DEFAULT_PORT}, dns};
use serde::{Deserialize, Serialize};
#[derive(Eq, Hash, PartialEq, Clone, Copy)]
#[derive(Debug, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)]
pub enum Position {
Left,
Right,
@@ -10,97 +15,192 @@ pub enum Position {
Bottom,
}
#[derive(Clone, Copy)]
pub struct Client {
pub addr: SocketAddr,
pub pos: Position,
pub handle: ClientHandle,
}
impl Client {
pub fn handle(&self) -> ClientHandle {
return self.handle;
impl Default for Position {
fn default() -> Self {
Self::Left
}
}
pub enum ClientEvent {
Create(Client),
Destroy(Client),
impl Position {
pub fn opposite(&self) -> Self {
match self {
Position::Left => Self::Right,
Position::Right => Self::Left,
Position::Top => Self::Bottom,
Position::Bottom => Self::Top,
}
}
}
pub struct ClientManager {
next_id: AtomicU32,
clients: RwLock<Vec<Client>>,
subscribers: RwLock<Vec<Arc<AtomicBool>>>,
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",
}
)
}
}
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
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
/// e.g. Laptops usually have at least an ethernet and a wifi port
/// which have different ip addresses
pub addrs: HashSet<SocketAddr>,
/// both active_addr and addrs can be None / empty so port needs to be stored seperately
pub port: u16,
/// position of a client on screen
pub pos: Position,
}
#[derive(Debug)]
pub enum ClientEvent {
Create(ClientHandle, Position),
Destroy(ClientHandle),
}
pub type ClientHandle = u32;
#[derive(Debug)]
struct ClientConfigError;
impl Display for ClientConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "neither ip nor hostname specified")
}
#[derive(Debug, Clone)]
pub struct ClientState {
pub client: Client,
pub active: bool,
pub last_ping: Option<Instant>,
pub last_seen: Option<Instant>,
pub last_replied: Option<Instant>,
}
impl Error for ClientConfigError {}
pub struct ClientManager {
clients: Vec<Option<ClientState>>, // HashMap likely not beneficial
}
impl Default for ClientManager {
fn default() -> Self {
Self::new()
}
}
impl ClientManager {
fn add_client(&self, client: &config::Client, pos: Position) -> Result<(), Box<dyn Error>> {
let ip = match client.ip {
Some(ip) => ip,
None => match &client.host_name {
Some(host_name) => dns::resolve(host_name)?,
None => return Err(Box::new(ClientConfigError{})),
},
};
let addr = SocketAddr::new(ip, client.port.unwrap_or(DEFAULT_PORT));
self.register_client(addr, pos);
Ok(())
pub fn new() -> Self {
Self { clients: vec![] }
}
fn notify(&self) {
for subscriber in self.subscribers.read().unwrap().iter() {
subscriber.store(true, Ordering::SeqCst);
}
}
/// add a new client to this manager
pub fn add_client(
&mut self,
hostname: Option<String>,
ips: HashSet<IpAddr>,
port: u16,
pos: Position,
) -> ClientHandle {
// get a new client_handle
let handle = self.free_id();
fn new_id(&self) -> ClientHandle {
let id = self.next_id.load(Ordering::Acquire);
self.next_id.store(id + 1, Ordering::Release);
id as ClientHandle
}
// we dont know, which IP is initially active
let active_addr = None;
pub fn new(config: &config::Config) -> Result<Self, Box<dyn Error>> {
// store fix ip addresses
let fix_ips = ips.iter().cloned().collect();
let client_manager = ClientManager {
next_id: AtomicU32::new(0),
clients: RwLock::new(Vec::new()),
subscribers: RwLock::new(vec![]),
// map ip addresses to socket addresses
let addrs = HashSet::from_iter(ips.into_iter().map(|ip| SocketAddr::new(ip, port)));
// store the client
let client = Client {
hostname,
fix_ips,
handle,
active_addr,
addrs,
port,
pos,
};
// add clients from config
for (client, pos) in config.clients.iter() {
client_manager.add_client(&client, *pos)?;
// client was never seen, nor pinged
let client_state = ClientState {
client,
last_ping: None,
last_seen: None,
last_replied: None,
active: false,
};
if handle as usize >= self.clients.len() {
assert_eq!(handle as usize, self.clients.len());
self.clients.push(Some(client_state));
} else {
self.clients[handle as usize] = Some(client_state);
}
Ok(client_manager)
handle
}
pub fn register_client(&self, addr: SocketAddr, pos: Position) {
let handle = self.new_id();
let client = Client { addr, pos, handle };
self.clients.write().unwrap().push(client);
self.notify();
/// find a client by its address
pub fn get_client(&self, addr: SocketAddr) -> Option<ClientHandle> {
// since there shouldn't be more than a handful of clients at any given
// time this is likely faster than using a HashMap
self.clients
.iter()
.position(|c| {
if let Some(c) = c {
c.active && c.client.addrs.contains(&addr)
} else {
false
}
})
.map(|p| p as ClientHandle)
}
pub fn get_clients(&self) -> Vec<Client> {
self.clients.read().unwrap().clone()
/// remove a client from the list
pub fn remove_client(&mut self, client: ClientHandle) -> Option<ClientState> {
// remove id from occupied ids
self.clients.get_mut(client as usize)?.take()
}
pub fn subscribe(&self, subscriber: Arc<AtomicBool>) {
self.subscribers.write().unwrap().push(subscriber);
/// get a free slot in the client list
fn free_id(&mut self) -> ClientHandle {
for i in 0..u32::MAX {
if self.clients.get(i as usize).is_none()
|| self.clients.get(i as usize).unwrap().is_none()
{
return i;
}
}
panic!("Out of client ids");
}
// returns an immutable reference to the client state corresponding to `client`
pub fn get(&self, client: ClientHandle) -> Option<&ClientState> {
self.clients.get(client as usize)?.as_ref()
}
/// returns a mutable reference to the client state corresponding to `client`
pub fn get_mut(&mut self, client: ClientHandle) -> Option<&mut ClientState> {
self.clients.get_mut(client as usize)?.as_mut()
}
pub fn get_client_states(&self) -> impl Iterator<Item = &ClientState> {
self.clients.iter().filter_map(|x| x.as_ref())
}
pub fn get_client_states_mut(&mut self) -> impl Iterator<Item = &mut ClientState> {
self.clients.iter_mut().filter_map(|x| x.as_mut())
}
}

View File

@@ -1,9 +1,10 @@
use serde_derive::{Deserialize, Serialize};
use core::fmt;
use anyhow::Result;
use clap::Parser;
use serde::{Deserialize, Serialize};
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;
@@ -13,7 +14,7 @@ pub const DEFAULT_PORT: u16 = 4242;
#[derive(Serialize, Deserialize, Debug)]
pub struct ConfigToml {
pub port: Option<u16>,
pub backend: Option<String>,
pub frontend: Option<String>,
pub left: Option<Client>,
pub right: Option<Client>,
pub top: Option<Client>,
@@ -23,77 +24,108 @@ pub struct ConfigToml {
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Client {
pub host_name: Option<String>,
pub ip: Option<IpAddr>,
pub ips: Option<Vec<IpAddr>>,
pub port: Option<u16>,
}
#[derive(Debug, Clone)]
struct MissingParameter {
arg: &'static str,
}
impl fmt::Display for MissingParameter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Missing a parameter for argument: {}", self.arg)
}
}
impl Error for MissingParameter {}
impl ConfigToml {
pub fn new(path: &str) -> Result<ConfigToml, Box<dyn Error>> {
let config = fs::read_to_string(path)?;
log::info!("using config: \"{path}\"");
Ok(toml::from_str::<_>(&config)?)
}
}
fn find_arg(key: &'static str) -> Result<Option<String>, MissingParameter> {
let args: Vec<String> = env::args().collect();
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct CliArgs {
/// the listen port for lan-mouse
#[arg(short, long)]
port: Option<u16>,
for (i, arg) in args.iter().enumerate() {
if arg != key {
continue;
}
match args.get(i+1) {
None => return Err(MissingParameter { arg: key }),
Some(arg) => return Ok(Some(arg.clone())),
};
}
Ok(None)
/// the frontend to use [cli | gtk]
#[arg(short, long)]
frontend: Option<String>,
/// non-default config file location
#[arg(short, long)]
config: Option<String>,
/// run only the service as a daemon without the frontend
#[arg(short, long)]
daemon: bool,
}
#[derive(Debug, PartialEq, Eq)]
pub enum Frontend {
Gtk,
Cli,
}
#[derive(Debug)]
pub struct Config {
pub backend: Option<String>,
pub frontend: Frontend,
pub port: u16,
pub clients: Vec<(Client, Position)>,
pub daemon: bool,
}
impl Config {
pub fn new() -> Result<Self, Box<dyn Error>> {
let config_path = "config.toml";
let config_toml = match ConfigToml::new(config_path) {
pub fn new() -> Result<Self> {
let args = CliArgs::parse();
let config_file = "config.toml";
#[cfg(unix)]
let config_path = {
let xdg_config_home =
env::var("XDG_CONFIG_HOME").unwrap_or(format!("{}/.config", env::var("HOME")?));
format!("{xdg_config_home}/lan-mouse/{config_file}")
};
#[cfg(not(unix))]
let config_path = {
let app_data =
env::var("LOCALAPPDATA").unwrap_or(format!("{}/.config", env::var("USERPROFILE")?));
format!("{app_data}\\lan-mouse\\{config_file}")
};
// --config <file> overrules default location
let config_path = args.config.unwrap_or(config_path);
let config_toml = match ConfigToml::new(config_path.as_str()) {
Err(e) => {
eprintln!("config.toml: {e}");
eprintln!("Continuing without config file ...");
log::error!("{config_path}: {e}");
log::warn!("Continuing without config file ...");
None
},
}
Ok(c) => Some(c),
};
let backend = match find_arg("--backend")? {
let frontend = match args.frontend {
None => match &config_toml {
Some(c) => c.backend.clone(),
Some(c) => c.frontend.clone(),
None => None,
},
backend => backend,
frontend => frontend,
};
let port = match find_arg("--port")? {
Some(port) => port.parse::<u16>()?,
let frontend = match frontend {
#[cfg(feature = "gtk")]
None => Frontend::Gtk,
#[cfg(not(feature = "gtk"))]
None => Frontend::Cli,
Some(s) => match s.as_str() {
"cli" => Frontend::Cli,
"gtk" => Frontend::Gtk,
_ => Frontend::Cli,
},
};
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![];
@@ -113,6 +145,29 @@ impl Config {
}
}
Ok(Config { backend, clients, port })
let daemon = args.daemon;
Ok(Config {
daemon,
frontend,
clients,
port,
})
}
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()
}
}

View File

@@ -1,76 +1,78 @@
use std::{thread::{JoinHandle, self}, sync::mpsc::Receiver, error::Error};
use async_trait::async_trait;
use std::future;
#[cfg(unix)]
use std::env;
use crate::{
backend::consumer,
client::{ClientEvent, ClientHandle},
event::Event,
};
use anyhow::Result;
use crate::{backend::consumer, client::{Client, ClientHandle}, event::Event};
#[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(())
}
#[cfg(unix)]
#[derive(Debug)]
enum Backend {
Wlroots,
X11,
RemoteDesktopPortal,
Libei,
async fn destroy(&mut self);
}
pub fn start(consume_rx: Receiver<(Event, ClientHandle)>, clients: Vec<Client>, backend: Option<String>) -> Result<JoinHandle<()>, Box<dyn Error>> {
pub async fn create() -> Box<dyn EventConsumer> {
#[cfg(windows)]
let _backend = backend;
match consumer::windows::WindowsConsumer::new() {
Ok(c) => return Box::new(c),
Err(e) => log::warn!("windows event consumer unavailable: {e}"),
}
Ok(thread::Builder::new()
.name("event consumer".into())
.spawn(move || {
#[cfg(windows)]
consumer::windows::run(consume_rx, clients);
#[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(unix)]
let backend = match env::var("XDG_SESSION_TYPE") {
Ok(session_type) => match session_type.as_str() {
"x11" => Backend::X11,
"wayland" => {
match backend {
Some(backend) => match backend.as_str() {
"wlroots" => Backend::Wlroots,
"libei" => Backend::Libei,
"xdg_desktop_portal" => Backend::RemoteDesktopPortal,
backend => panic!("invalid backend: {}", backend)
}
// default to wlroots backend for now
_ => Backend::Wlroots,
}
}
_ => panic!("unknown XDG_SESSION_TYPE"),
},
Err(_) => panic!("could not detect session type: XDG_SESSION_TYPE environment variable not set!"),
};
#[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(unix)]
match backend {
Backend::Libei => {
#[cfg(not(feature = "libei"))]
panic!("feature libei not enabled");
#[cfg(feature = "libei")]
consumer::libei::run(consume_rx, clients);
},
Backend::RemoteDesktopPortal => {
#[cfg(not(feature = "xdg_desktop_portal"))]
panic!("feature xdg_desktop_portal not enabled");
#[cfg(feature = "xdg_desktop_portal")]
consumer::xdg_desktop_portal::run(consume_rx, clients);
},
Backend::Wlroots => {
#[cfg(not(feature = "wayland"))]
panic!("feature wayland not enabled");
#[cfg(feature = "wayland")]
consumer::wlroots::run(consume_rx, clients);
},
Backend::X11 => {
#[cfg(not(feature = "x11"))]
panic!("feature x11 not enabled");
#[cfg(feature = "x11")]
consumer::x11::run(consume_rx, clients);
},
}
})?)
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
match consumer::libei::LibeiConsumer::new().await {
Ok(c) => {
log::info!("using libei event consumer");
return Box::new(c);
}
Err(e) => log::info!("libei not available: {e}"),
}
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
match consumer::xdg_desktop_portal::DesktopPortalConsumer::new().await {
Ok(c) => {
log::info!("using xdg-remote-desktop-portal event consumer");
return Box::new(c);
}
Err(e) => log::info!("remote desktop portal not available: {e}"),
}
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
match consumer::x11::X11Consumer::new() {
Ok(c) => {
log::info!("using x11 event consumer");
return Box::new(c);
}
Err(e) => log::info!("x11 consumer not available: {e}"),
}
log::error!("falling back to dummy event consumer");
Box::new(consumer::dummy::DummyConsumer::new())
}

View File

@@ -1,27 +1,23 @@
use std::{error::Error, fmt::Display, net::IpAddr};
use anyhow::Result;
use std::{error::Error, net::IpAddr};
use trust_dns_resolver::Resolver;
use trust_dns_resolver::TokioAsyncResolver;
#[derive(Debug, Clone)]
struct InvalidConfigError;
#[derive(Debug, Clone)]
struct DnsError {
host: String,
pub struct DnsResolver {
resolver: TokioAsyncResolver,
}
impl DnsResolver {
pub(crate) async fn new() -> Result<Self> {
let resolver = TokioAsyncResolver::tokio_from_system_conf()?;
Ok(Self { resolver })
}
impl Error for DnsError {}
impl Display for DnsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "couldn't resolve host \"{}\"", self.host)
}
}
pub fn resolve(host: &String) -> Result<IpAddr, Box<dyn Error>> {
let response = Resolver::from_system_conf()?.lookup_ip(host)?;
match response.iter().next() {
Some(ip) => Ok(ip),
None => Err(DnsError { host: host.clone() }.into()),
pub(crate) async fn resolve(&self, host: &str) -> Result<Vec<IpAddr>, Box<dyn Error>> {
log::info!("resolving {host} ...");
let response = self.resolver.lookup_ip(host).await?;
for ip in response.iter() {
log::info!("{host}: adding ip {ip}");
}
Ok(response.iter().collect())
}
}

View File

@@ -1,7 +1,16 @@
use std::{error::Error, fmt};
use std::{
error::Error,
fmt::{self, Display},
};
pub mod server;
// FIXME
pub const BTN_LEFT: u32 = 0x110;
pub const BTN_RIGHT: u32 = 0x111;
pub const BTN_MIDDLE: u32 = 0x112;
pub const BTN_BACK: u32 = 0x113;
pub const BTN_FORWARD: u32 = 0x114;
#[derive(Debug, Clone, Copy)]
pub enum PointerEvent {
Motion {
time: u32,
@@ -21,6 +30,7 @@ pub enum PointerEvent {
Frame {},
}
#[derive(Debug, Clone, Copy)]
pub enum KeyboardEvent {
Key {
time: u32,
@@ -35,21 +45,94 @@ pub enum KeyboardEvent {
},
}
#[derive(Debug, Clone, Copy)]
pub enum Event {
/// pointer event (motion / button / axis)
Pointer(PointerEvent),
/// keyboard events (key / modifiers)
Keyboard(KeyboardEvent),
Release(),
/// enter event: request to enter a client.
/// The client must release the pointer if it is grabbed
/// and reply with a leave event, as soon as its ready to
/// receive events
Enter(),
/// leave event: this client is now ready to receive events and will
/// not send any events after until it sends an enter event
Leave(),
/// ping a client, to see if it is still alive. A client that does
/// not respond with a pong event will be assumed to be offline.
Ping(),
/// response to a ping event: this event signals that a client
/// is still alive but must otherwise be ignored
Pong(),
}
unsafe impl Send for Event {}
unsafe impl Sync for Event {}
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()"),
}
}
}
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})"
),
}
}
}
impl Display for Event {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Event::Pointer(p) => write!(f, "{}", p),
Event::Keyboard(k) => write!(f, "{}", k),
Event::Enter() => write!(f, "enter"),
Event::Leave() => write!(f, "leave"),
Event::Ping() => write!(f, "ping"),
Event::Pong() => write!(f, "pong"),
}
}
}
impl Event {
fn event_type(&self) -> EventType {
match self {
Self::Pointer(_) => EventType::POINTER,
Self::Keyboard(_) => EventType::KEYBOARD,
Self::Release() => EventType::RELEASE,
Self::Pointer(_) => EventType::Pointer,
Self::Keyboard(_) => EventType::Keyboard,
Self::Enter() => EventType::Enter,
Self::Leave() => EventType::Leave,
Self::Ping() => EventType::Ping,
Self::Pong() => EventType::Pong,
}
}
}
@@ -57,10 +140,10 @@ impl Event {
impl PointerEvent {
fn event_type(&self) -> PointerEventType {
match self {
Self::Motion { .. } => PointerEventType::MOTION,
Self::Button { .. } => PointerEventType::BUTTON,
Self::Axis { .. } => PointerEventType::AXIS,
Self::Frame { .. } => PointerEventType::FRAME,
Self::Motion { .. } => PointerEventType::Motion,
Self::Button { .. } => PointerEventType::Button,
Self::Axis { .. } => PointerEventType::Axis,
Self::Frame { .. } => PointerEventType::Frame,
}
}
}
@@ -68,26 +151,29 @@ impl PointerEvent {
impl KeyboardEvent {
fn event_type(&self) -> KeyboardEventType {
match self {
KeyboardEvent::Key { .. } => KeyboardEventType::KEY,
KeyboardEvent::Modifiers { .. } => KeyboardEventType::MODIFIERS,
KeyboardEvent::Key { .. } => KeyboardEventType::Key,
KeyboardEvent::Modifiers { .. } => KeyboardEventType::Modifiers,
}
}
}
enum PointerEventType {
MOTION,
BUTTON,
AXIS,
FRAME,
Motion,
Button,
Axis,
Frame,
}
enum KeyboardEventType {
KEY,
MODIFIERS,
Key,
Modifiers,
}
enum EventType {
POINTER,
KEYBOARD,
RELEASE,
Pointer,
Keyboard,
Enter,
Leave,
Ping,
Pong,
}
impl TryFrom<u8> for PointerEventType {
@@ -95,10 +181,10 @@ impl TryFrom<u8> for PointerEventType {
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
x if x == Self::MOTION as u8 => Ok(Self::MOTION),
x if x == Self::BUTTON as u8 => Ok(Self::BUTTON),
x if x == Self::AXIS as u8 => Ok(Self::AXIS),
x if x == Self::FRAME as u8 => Ok(Self::FRAME),
x if x == Self::Motion as u8 => Ok(Self::Motion),
x if x == Self::Button as u8 => Ok(Self::Button),
x if x == Self::Axis as u8 => Ok(Self::Axis),
x if x == Self::Frame as u8 => Ok(Self::Frame),
_ => Err(Box::new(ProtocolError {
msg: format!("invalid pointer event type {}", value),
})),
@@ -111,8 +197,8 @@ impl TryFrom<u8> for KeyboardEventType {
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
x if x == Self::KEY as u8 => Ok(Self::KEY),
x if x == Self::MODIFIERS as u8 => Ok(Self::MODIFIERS),
x if x == Self::Key as u8 => Ok(Self::Key),
x if x == Self::Modifiers as u8 => Ok(Self::Modifiers),
_ => Err(Box::new(ProtocolError {
msg: format!("invalid keyboard event type {}", value),
})),
@@ -120,15 +206,18 @@ impl TryFrom<u8> for KeyboardEventType {
}
}
impl Into<Vec<u8>> for &Event {
fn into(self) -> Vec<u8> {
let event_id = vec![self.event_type() as u8];
let event_data = match self {
impl From<&Event> for Vec<u8> {
fn from(event: &Event) -> Self {
let event_id = vec![event.event_type() as u8];
let event_data = match event {
Event::Pointer(p) => p.into(),
Event::Keyboard(k) => k.into(),
Event::Release() => vec![],
Event::Enter() => vec![],
Event::Leave() => vec![],
Event::Ping() => vec![],
Event::Pong() => vec![],
};
vec![event_id, event_data].concat()
[event_id, event_data].concat()
}
}
@@ -150,9 +239,12 @@ impl TryFrom<Vec<u8>> for Event {
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
let event_id = u8::from_be_bytes(value[..1].try_into()?);
match event_id {
i if i == (EventType::POINTER as u8) => Ok(Event::Pointer(value.try_into()?)),
i if i == (EventType::KEYBOARD as u8) => Ok(Event::Keyboard(value.try_into()?)),
i if i == (EventType::RELEASE as u8) => Ok(Event::Release()),
i if i == (EventType::Pointer as u8) => Ok(Event::Pointer(value.try_into()?)),
i if i == (EventType::Keyboard as u8) => Ok(Event::Keyboard(value.try_into()?)),
i if i == (EventType::Enter as u8) => Ok(Event::Enter()),
i if i == (EventType::Leave as u8) => Ok(Event::Leave()),
i if i == (EventType::Ping as u8) => Ok(Event::Ping()),
i if i == (EventType::Pong as u8) => Ok(Event::Pong()),
_ => Err(Box::new(ProtocolError {
msg: format!("invalid event_id {}", event_id),
})),
@@ -160,10 +252,10 @@ impl TryFrom<Vec<u8>> for Event {
}
}
impl Into<Vec<u8>> for &PointerEvent {
fn into(self) -> Vec<u8> {
let id = vec![self.event_type() as u8];
let data = match self {
impl From<&PointerEvent> for Vec<u8> {
fn from(event: &PointerEvent) -> Self {
let id = vec![event.event_type() as u8];
let data = match event {
PointerEvent::Motion {
time,
relative_x,
@@ -172,7 +264,7 @@ impl Into<Vec<u8>> for &PointerEvent {
let time = time.to_be_bytes();
let relative_x = relative_x.to_be_bytes();
let relative_y = relative_y.to_be_bytes();
vec![&time[..], &relative_x[..], &relative_y[..]].concat()
[&time[..], &relative_x[..], &relative_y[..]].concat()
}
PointerEvent::Button {
time,
@@ -182,19 +274,19 @@ impl Into<Vec<u8>> for &PointerEvent {
let time = time.to_be_bytes();
let button = button.to_be_bytes();
let state = state.to_be_bytes();
vec![&time[..], &button[..], &state[..]].concat()
[&time[..], &button[..], &state[..]].concat()
}
PointerEvent::Axis { time, axis, value } => {
let time = time.to_be_bytes();
let axis = axis.to_be_bytes();
let value = value.to_be_bytes();
vec![&time[..], &axis[..], &value[..]].concat()
[&time[..], &axis[..], &value[..]].concat()
}
PointerEvent::Frame {} => {
vec![]
}
};
vec![id, data].concat()
[id, data].concat()
}
}
@@ -209,7 +301,7 @@ impl TryFrom<Vec<u8>> for PointerEvent {
Err(e) => return Err(e),
};
match event_type {
PointerEventType::MOTION => {
PointerEventType::Motion => {
let time = match data.get(2..6) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
@@ -240,7 +332,7 @@ impl TryFrom<Vec<u8>> for PointerEvent {
relative_y,
})
}
PointerEventType::BUTTON => {
PointerEventType::Button => {
let time = match data.get(2..6) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
@@ -271,7 +363,7 @@ impl TryFrom<Vec<u8>> for PointerEvent {
state,
})
}
PointerEventType::AXIS => {
PointerEventType::Axis => {
let time = match data.get(2..6) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
@@ -298,7 +390,7 @@ impl TryFrom<Vec<u8>> for PointerEvent {
};
Ok(Self::Axis { time, axis, value })
}
PointerEventType::FRAME => Ok(Self::Frame {}),
PointerEventType::Frame => Ok(Self::Frame {}),
}
}
None => Err(Box::new(ProtocolError {
@@ -308,15 +400,15 @@ impl TryFrom<Vec<u8>> for PointerEvent {
}
}
impl Into<Vec<u8>> for &KeyboardEvent {
fn into(self) -> Vec<u8> {
let id = vec![self.event_type() as u8];
let data = match self {
impl From<&KeyboardEvent> for Vec<u8> {
fn from(event: &KeyboardEvent) -> Self {
let id = vec![event.event_type() as u8];
let data = match event {
KeyboardEvent::Key { time, key, state } => {
let time = time.to_be_bytes();
let key = key.to_be_bytes();
let state = state.to_be_bytes();
vec![&time[..], &key[..], &state[..]].concat()
[&time[..], &key[..], &state[..]].concat()
}
KeyboardEvent::Modifiers {
mods_depressed,
@@ -328,7 +420,7 @@ impl Into<Vec<u8>> for &KeyboardEvent {
let mods_latched = mods_latched.to_be_bytes();
let mods_locked = mods_locked.to_be_bytes();
let group = group.to_be_bytes();
vec![
[
&mods_depressed[..],
&mods_latched[..],
&mods_locked[..],
@@ -337,7 +429,7 @@ impl Into<Vec<u8>> for &KeyboardEvent {
.concat()
}
};
vec![id, data].concat()
[id, data].concat()
}
}
@@ -352,7 +444,7 @@ impl TryFrom<Vec<u8>> for KeyboardEvent {
Err(e) => return Err(e),
};
match event_type {
KeyboardEventType::KEY => {
KeyboardEventType::Key => {
let time = match data.get(2..6) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
@@ -379,7 +471,7 @@ impl TryFrom<Vec<u8>> for KeyboardEvent {
};
Ok(KeyboardEvent::Key { time, key, state })
}
KeyboardEventType::MODIFIERS => {
KeyboardEventType::Modifiers => {
let mods_depressed = match data.get(2..6) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {

View File

@@ -1,166 +0,0 @@
use anyhow::Result;
use std::{
collections::HashMap,
error::Error,
net::{SocketAddr, UdpSocket},
sync::{
atomic::{AtomicBool, Ordering},
mpsc::{Receiver, SyncSender},
Arc,
},
thread::{self, JoinHandle},
};
use crate::{client::{ClientHandle, ClientManager}, ioutils::{ask_confirmation, ask_position}};
use super::Event;
pub struct Server {
listen_addr: SocketAddr,
sending: Arc<AtomicBool>,
}
impl Server {
pub fn new(port: u16) -> Result<Self, Box<dyn Error>> {
let listen_addr = SocketAddr::new("0.0.0.0".parse()?, port);
let sending = Arc::new(AtomicBool::new(false));
Ok(Server {
listen_addr,
sending,
})
}
pub fn run(
&self,
client_manager: Arc<ClientManager>,
produce_rx: Receiver<(Event, ClientHandle)>,
consume_tx: SyncSender<(Event, ClientHandle)>,
) -> Result<(JoinHandle<Result<()>>, JoinHandle<Result<()>>), Box<dyn Error>> {
let udp_socket = UdpSocket::bind(self.listen_addr)?;
let rx = udp_socket.try_clone()?;
let tx = udp_socket;
let sending = self.sending.clone();
let clients_updated = Arc::new(AtomicBool::new(true));
client_manager.subscribe(clients_updated.clone());
let client_manager_clone = client_manager.clone();
let receiver = thread::Builder::new()
.name("event receiver".into())
.spawn(move || {
let mut client_for_socket = HashMap::new();
loop {
let (event, addr) = match Server::receive_event(&rx) {
Ok(e) => e,
Err(e) => {
eprintln!("{}", e);
continue;
}
};
if let Ok(_) = clients_updated.compare_exchange(
true,
false,
Ordering::SeqCst,
Ordering::SeqCst,
) {
clients_updated.store(false, Ordering::SeqCst);
client_for_socket.clear();
println!("updating clients: ");
for client in client_manager_clone.get_clients() {
println!("{}: {}", client.handle, client.addr);
client_for_socket.insert(client.addr, client.handle);
}
}
let client_handle = match client_for_socket.get(&addr) {
Some(c) => *c,
None => {
eprint!("Allow connection from {:?}? ", addr);
if ask_confirmation(false)? {
client_manager_clone.register_client(addr, ask_position()?);
} else {
eprintln!("rejecting client: {:?}?", addr);
}
continue;
}
};
// There is a race condition between loading this
// value and handling the event:
// In the meantime a event could be produced, which
// should theoretically disable receiving of events.
//
// This is however not a huge problem, as some
// events that make it through are not a large problem
if sending.load(Ordering::Acquire) {
// ignore received events when in sending state
// if release event is received, switch state to receiving
if let Event::Release() = event {
sending.store(false, Ordering::Release);
consume_tx
.send((event, client_handle))
.expect("event consumer unavailable");
}
} else {
if let Event::Release() = event {
sending.store(false, Ordering::Release);
}
// we retrieve all events
consume_tx
.send((event, client_handle))
.expect("event consumer unavailable");
}
}
})?;
let sending = self.sending.clone();
let mut socket_for_client = HashMap::new();
for client in client_manager.get_clients() {
socket_for_client.insert(client.handle, client.addr);
}
let sender = thread::Builder::new()
.name("event sender".into())
.spawn(move || {
loop {
let (event, client_handle) =
produce_rx.recv().expect("event producer unavailable");
let addr = match socket_for_client.get(&client_handle) {
Some(addr) => addr,
None => continue,
};
if sending.load(Ordering::Acquire) {
Server::send_event(&tx, event, *addr);
} else {
// only accept enter event
if let Event::Release() = event {
// set state to sending, to ignore incoming events
// and enable sending of events
sending.store(true, Ordering::Release);
Server::send_event(&tx, event, *addr);
}
}
}
})?;
Ok((receiver, sender))
}
fn send_event(tx: &UdpSocket, e: Event, addr: SocketAddr) {
let data: Vec<u8> = (&e).into();
if let Err(e) = tx.send_to(&data[..], addr) {
eprintln!("{}", e);
}
}
fn receive_event(rx: &UdpSocket) -> Result<(Event, SocketAddr), Box<dyn Error>> {
let mut buf = vec![0u8; 22];
match rx.recv_from(&mut buf) {
Ok((_amt, src)) => Ok((Event::try_from(buf)?, src)),
Err(e) => Err(Box::new(e)),
}
}
}

271
src/frontend.rs Normal file
View File

@@ -0,0 +1,271 @@
use anyhow::{anyhow, Result};
use std::{cmp::min, io::ErrorKind, str, time::Duration};
#[cfg(unix)]
use std::{
env,
path::{Path, PathBuf},
};
use tokio::io::ReadHalf;
use tokio::io::{AsyncReadExt, AsyncWriteExt, WriteHalf};
#[cfg(unix)]
use tokio::net::UnixListener;
#[cfg(unix)]
use tokio::net::UnixStream;
#[cfg(windows)]
use tokio::net::TcpListener;
#[cfg(windows)]
use tokio::net::TcpStream;
use serde::{Deserialize, Serialize};
use crate::{
client::{Client, ClientHandle, Position},
config::{Config, Frontend},
};
/// cli frontend
pub mod cli;
/// gtk frontend
#[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),
/// 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),
NotifyClientDelete(ClientHandle),
/// new port, reason of failure (if failed)
NotifyPortChange(u16, Option<String>),
Enumerate(Vec<(Client, bool)>),
NotifyError(String),
}
pub struct FrontendListener {
#[cfg(windows)]
listener: TcpListener,
#[cfg(unix)]
listener: UnixListener,
#[cfg(unix)]
socket_path: PathBuf,
#[cfg(unix)]
tx_streams: Vec<WriteHalf<UnixStream>>,
#[cfg(windows)]
tx_streams: Vec<WriteHalf<TcpStream>>,
}
impl FrontendListener {
#[cfg(all(unix, not(target_os = "macos")))]
pub fn socket_path() -> Result<PathBuf> {
let xdg_runtime_dir = match env::var("XDG_RUNTIME_DIR") {
Ok(d) => d,
Err(e) => return Err(anyhow!("could not find XDG_RUNTIME_DIR: {e}")),
};
let xdg_runtime_dir = Path::new(xdg_runtime_dir.as_str());
Ok(xdg_runtime_dir.join("lan-mouse-socket.sock"))
}
#[cfg(all(unix, target_os = "macos"))]
pub fn socket_path() -> Result<PathBuf> {
let home = match env::var("HOME") {
Ok(d) => d,
Err(e) => return Err(anyhow!("could not find HOME: {e}")),
};
let home = Path::new(home.as_str());
let path = home
.join("Library")
.join("Caches")
.join("lan-mouse-socket.sock");
Ok(path)
}
pub async fn new() -> Option<Result<Self>> {
#[cfg(unix)]
let (socket_path, 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 = match TcpListener::bind("127.0.0.1:5252").await {
Ok(ls) => ls,
// some other lan-mouse instance has bound the socket in the meantime
Err(e) if e.kind() == ErrorKind::AddrInUse => return None,
Err(e) => return Some(Err(anyhow!("failed to bind lan-mouse-socket: {e}"))),
};
let adapter = Self {
listener,
#[cfg(unix)]
socket_path,
tx_streams: vec![],
};
Some(Ok(adapter))
}
#[cfg(unix)]
pub async fn accept(&mut self) -> Result<ReadHalf<UnixStream>> {
let stream = self.listener.accept().await?.0;
let (rx, tx) = tokio::io::split(stream);
self.tx_streams.push(tx);
Ok(rx)
}
#[cfg(windows)]
pub async fn accept(&mut self) -> Result<ReadHalf<TcpStream>> {
let stream = self.listener.accept().await?.0;
let (rx, tx) = tokio::io::split(stream);
self.tx_streams.push(tx);
Ok(rx)
}
pub(crate) async fn notify_all(&mut self, notify: FrontendNotify) -> Result<()> {
// encode event
let json = serde_json::to_string(&notify).unwrap();
let payload = json.as_bytes();
let len = payload.len().to_be_bytes();
log::debug!("json: {json}, len: {}", payload.len());
let mut keep = vec![];
// TODO do simultaneously
for tx in self.tx_streams.iter_mut() {
// write len + payload
if tx.write(&len).await.is_err() {
keep.push(false);
continue;
}
if tx.write(payload).await.is_err() {
keep.push(false);
continue;
}
keep.push(true);
}
// could not find a better solution because async
let mut keep = keep.into_iter();
self.tx_streams.retain(|_| keep.next().unwrap());
Ok(())
}
}
#[cfg(unix)]
impl Drop for FrontendListener {
fn drop(&mut self) {
log::debug!("remove socket: {:?}", self.socket_path);
let _ = std::fs::remove_file(&self.socket_path);
}
}
#[cfg(unix)]
pub async fn read_event(stream: &mut ReadHalf<UnixStream>) -> Result<FrontendEvent> {
let len = stream.read_u64().await?;
assert!(len <= 256);
let mut buf = [0u8; 256];
stream.read_exact(&mut buf[..len as usize]).await?;
Ok(serde_json::from_slice(&buf[..len as usize])?)
}
#[cfg(windows)]
pub async fn read_event(stream: &mut ReadHalf<TcpStream>) -> Result<FrontendEvent> {
let len = stream.read_u64().await?;
let mut buf = [0u8; 256];
stream.read_exact(&mut buf[..len as usize]).await?;
Ok(serde_json::from_slice(&buf[..len as usize])?)
}

233
src/frontend/cli.rs Normal file
View File

@@ -0,0 +1,233 @@
use anyhow::{anyhow, Context, Result};
use std::{
io::{ErrorKind, Read, Write},
str::SplitWhitespace,
thread,
};
use crate::{client::Position, config::DEFAULT_PORT};
use super::{FrontendEvent, FrontendNotify};
pub fn run() -> Result<()> {
let Ok(mut tx) = super::wait_for_service() else {
return Err(anyhow!("Could not connect to lan-mouse-socket"));
};
let mut rx = tx.try_clone()?;
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) => return,
Ok(len) => {
if let Some(events) = parse_cmd(buf, len) {
for event in events.iter() {
let json = serde_json::to_string(&event).unwrap();
let bytes = json.as_bytes();
let len = bytes.len().to_be_bytes();
if let Err(e) = tx.write(&len) {
log::error!("error sending message: {e}");
};
if let Err(e) = tx.write(bytes) {
log::error!("error sending message: {e}");
};
if *event == FrontendEvent::Shutdown() {
return;
}
}
// prompt is printed after the server response is received
} else {
prompt();
}
}
Err(e) => {
if e.kind() != ErrorKind::UnexpectedEof {
log::error!("error reading from stdin: {e}");
}
return;
}
}
}
})?;
let _ = thread::Builder::new()
.name("cli-frontend-notify".to_string())
.spawn(move || {
loop {
// read len
let mut len = [0u8; 8];
match rx.read_exact(&mut len) {
Ok(()) => (),
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break,
Err(e) => break log::error!("{e}"),
};
let len = usize::from_be_bytes(len);
// read payload
let mut buf: Vec<u8> = vec![0u8; len];
match rx.read_exact(&mut buf[..len]) {
Ok(()) => (),
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break,
Err(e) => break log::error!("{e}"),
};
let notify: FrontendNotify = match serde_json::from_slice(&buf) {
Ok(n) => n,
Err(e) => break log::error!("{e}"),
};
match notify {
FrontendNotify::NotifyClientCreate(client, host, port, pos) => {
log::info!(
"new client ({client}): {}:{port} - {pos}",
host.as_deref().unwrap_or("")
);
}
FrontendNotify::NotifyClientUpdate(client, host, port, pos) => {
log::info!(
"client ({client}) updated: {}:{port} - {pos}",
host.as_deref().unwrap_or("")
);
}
FrontendNotify::NotifyClientDelete(client) => {
log::info!("client ({client}) deleted.");
}
FrontendNotify::NotifyError(e) => {
log::warn!("{e}");
}
FrontendNotify::Enumerate(clients) => {
for (client, active) in clients.into_iter() {
log::info!(
"client ({}) [{}]: active: {}, associated addresses: [{}]",
client.handle,
client.hostname.as_deref().unwrap_or(""),
if active { "yes" } else { "no" },
client
.addrs
.into_iter()
.map(|a| a.to_string())
.collect::<Vec<String>>()
.join(", ")
);
}
}
FrontendNotify::NotifyPortChange(port, msg) => match msg {
Some(msg) => log::info!("could not change port: {msg}"),
None => log::info!("port changed: {port}"),
},
}
prompt();
}
})?;
match reader.join() {
Ok(_) => {}
Err(e) => {
let msg = match (e.downcast_ref::<&str>(), e.downcast_ref::<String>()) {
(Some(&s), _) => s,
(_, Some(s)) => s,
_ => "no panic info",
};
log::error!("reader thread paniced: {msg}");
}
}
Ok(())
}
fn prompt() {
eprint!("lan-mouse > ");
std::io::stderr().flush().unwrap();
}
fn parse_cmd(s: String, len: usize) -> Option<Vec<FrontendEvent>> {
if len == 0 {
return Some(vec![FrontendEvent::Shutdown()]);
}
let mut l = s.split_whitespace();
let cmd = l.next()?;
let res = match cmd {
"help" => {
log::info!("list list clients");
log::info!("connect <host> left|right|top|bottom [port] add a new client");
log::info!("disconnect <client> remove a client");
log::info!("activate <client> activate a client");
log::info!("deactivate <client> deactivate a client");
log::info!("exit exit lan-mouse");
log::info!("setport <port> change port");
None
}
"exit" => return Some(vec![FrontendEvent::Shutdown()]),
"list" => return Some(vec![FrontendEvent::Enumerate()]),
"connect" => Some(parse_connect(l)),
"disconnect" => Some(parse_disconnect(l)),
"activate" => Some(parse_activate(l)),
"deactivate" => Some(parse_deactivate(l)),
"setport" => Some(parse_port(l)),
_ => {
log::error!("unknown command: {s}");
None
}
};
match res {
Some(Ok(e)) => Some(e),
Some(Err(e)) => {
log::warn!("{e}");
None
}
_ => None,
}
}
fn parse_connect(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> {
let usage = "usage: connect <host> left|right|top|bottom [port]";
let host = l.next().context(usage)?.to_owned();
let pos = match l.next().context(usage)? {
"right" => Position::Right,
"top" => Position::Top,
"bottom" => Position::Bottom,
_ => Position::Left,
};
let port = if let Some(p) = l.next() {
p.parse()?
} else {
DEFAULT_PORT
};
Ok(vec![
FrontendEvent::AddClient(Some(host), port, pos),
FrontendEvent::Enumerate(),
])
}
fn parse_disconnect(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> {
let client = l.next().context("usage: disconnect <client_id>")?.parse()?;
Ok(vec![
FrontendEvent::DelClient(client),
FrontendEvent::Enumerate(),
])
}
fn parse_activate(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> {
let client = l.next().context("usage: activate <client_id>")?.parse()?;
Ok(vec![
FrontendEvent::ActivateClient(client, true),
FrontendEvent::Enumerate(),
])
}
fn parse_deactivate(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> {
let client = l.next().context("usage: deactivate <client_id>")?.parse()?;
Ok(vec![
FrontendEvent::ActivateClient(client, false),
FrontendEvent::Enumerate(),
])
}
fn parse_port(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> {
let port = l.next().context("usage: setport <port>")?.parse()?;
Ok(vec![FrontendEvent::ChangePort(port)])
}

208
src/frontend/gtk.rs Normal file
View File

@@ -0,0 +1,208 @@
mod client_object;
mod client_row;
mod window;
use std::{
env,
io::{ErrorKind, Read},
process, str,
};
use crate::{config::DEFAULT_PORT, frontend::gtk::window::Window};
use adw::Application;
use gtk::{
gdk::Display,
gio::{SimpleAction, SimpleActionGroup},
glib::clone,
prelude::*,
subclass::prelude::ObjectSubclassIsExt,
CssProvider, IconTheme,
};
use gtk::{gio, glib, prelude::ApplicationExt};
use self::client_object::ClientObject;
use super::FrontendNotify;
pub fn run() -> glib::ExitCode {
log::debug!("running gtk frontend");
#[cfg(windows)]
let ret = std::thread::Builder::new()
.stack_size(8 * 1024 * 1024) // https://gitlab.gnome.org/GNOME/gtk/-/commit/52dbb3f372b2c3ea339e879689c1de535ba2c2c3 -> caused crash on windows
.name("gtk".into())
.spawn(gtk_main)
.unwrap()
.join()
.unwrap();
#[cfg(not(windows))]
let ret = gtk_main();
log::debug!("frontend exited");
ret
}
fn gtk_main() -> glib::ExitCode {
gio::resources_register_include!("lan-mouse.gresource").expect("Failed to register resources.");
let app = Application::builder()
.application_id("de.feschber.lan-mouse")
.build();
app.connect_startup(|_| load_icons());
app.connect_startup(|_| load_css());
app.connect_activate(build_ui);
let args: Vec<&'static str> = vec![];
app.run_with_args(&args)
}
fn load_css() {
let provider = CssProvider::new();
provider.load_from_resource("de/feschber/LanMouse/style.css");
gtk::style_context_add_provider_for_display(
&Display::default().expect("Could not connect to a display."),
&provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}
fn load_icons() {
let icon_theme =
IconTheme::for_display(&Display::default().expect("Could not connect to a display."));
icon_theme.add_resource_path("/de/feschber/LanMouse/icons");
}
fn build_ui(app: &Application) {
log::debug!("connecting to lan-mouse-socket");
let mut rx = match super::wait_for_service() {
Ok(stream) => stream,
Err(e) => {
log::error!("could not connect to lan-mouse-socket: {e}");
process::exit(1);
}
};
let tx = match rx.try_clone() {
Ok(sock) => sock,
Err(e) => {
log::error!("{e}");
process::exit(1);
}
};
log::debug!("connected to lan-mouse-socket");
let (sender, receiver) = async_channel::bounded(10);
gio::spawn_blocking(move || {
match loop {
// read length
let mut len = [0u8; 8];
match rx.read_exact(&mut len) {
Ok(_) => (),
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()),
Err(e) => break Err(e),
};
let len = usize::from_be_bytes(len);
// read payload
let mut buf = vec![0u8; len];
match rx.read_exact(&mut buf) {
Ok(_) => (),
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()),
Err(e) => break Err(e),
};
// parse json
let json = str::from_utf8(&buf).unwrap();
match serde_json::from_str(json) {
Ok(notify) => sender.send_blocking(notify).unwrap(),
Err(e) => log::error!("{e}"),
}
} {
Ok(()) => {}
Err(e) => log::error!("{e}"),
}
});
let window = Window::new(app);
window.imp().stream.borrow_mut().replace(tx);
glib::spawn_future_local(clone!(@weak window => async move {
loop {
let notify = receiver.recv().await.unwrap();
match notify {
FrontendNotify::NotifyClientCreate(client, hostname, port, position) => {
window.new_client(client, hostname, port, position, false);
},
FrontendNotify::NotifyClientUpdate(client, hostname, port, position) => {
log::info!("client updated: {client}, {}:{port}, {position}", hostname.unwrap_or("".to_string()));
}
FrontendNotify::NotifyError(e) => {
// TODO
log::error!("{e}");
},
FrontendNotify::NotifyClientDelete(client) => {
window.delete_client(client);
}
FrontendNotify::Enumerate(clients) => {
for (client, active) in clients {
if window.client_idx(client.handle).is_some() {
continue
}
window.new_client(
client.handle,
client.hostname,
client.addrs
.iter()
.next()
.map(|s| s.port())
.unwrap_or(DEFAULT_PORT),
client.pos,
active,
);
}
},
FrontendNotify::NotifyPortChange(port, msg) => {
match msg {
None => window.show_toast(format!("port changed: {port}").as_str()),
Some(msg) => window.show_toast(msg.as_str()),
}
window.imp().set_port(port);
}
}
}
}));
let action_request_client_update =
SimpleAction::new("request-client-update", Some(&u32::static_variant_type()));
// remove client
let action_client_delete =
SimpleAction::new("request-client-delete", Some(&u32::static_variant_type()));
// update client state
action_request_client_update.connect_activate(clone!(@weak window => move |_action, param| {
log::debug!("request-client-update");
let index = param.unwrap()
.get::<u32>()
.unwrap();
let Some(client) = window.clients().item(index) else {
return;
};
let client = client.downcast_ref::<ClientObject>().unwrap();
window.request_client_update(client);
}));
action_client_delete.connect_activate(clone!(@weak window => move |_action, param| {
log::debug!("delete-client");
let idx = param.unwrap()
.get::<u32>()
.unwrap();
window.request_client_delete(idx);
}));
let actions = SimpleActionGroup::new();
window.insert_action_group("win", Some(&actions));
actions.add_action(&action_request_client_update);
actions.add_action(&action_client_delete);
window.present();
}

View File

@@ -0,0 +1,41 @@
mod imp;
use adw::subclass::prelude::*;
use gtk::glib::{self, Object};
use crate::client::ClientHandle;
glib::wrapper! {
pub struct ClientObject(ObjectSubclass<imp::ClientObject>);
}
impl ClientObject {
pub fn new(
handle: ClientHandle,
hostname: Option<String>,
port: u32,
position: String,
active: bool,
) -> Self {
Object::builder()
.property("handle", handle)
.property("hostname", hostname)
.property("port", port)
.property("active", active)
.property("position", position)
.build()
}
pub fn get_data(&self) -> ClientData {
self.imp().data.borrow().clone()
}
}
#[derive(Default, Clone)]
pub struct ClientData {
pub handle: ClientHandle,
pub hostname: Option<String>,
pub port: u32,
pub active: bool,
pub position: String,
}

View File

@@ -0,0 +1,30 @@
use std::cell::RefCell;
use glib::Properties;
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use crate::client::ClientHandle;
use super::ClientData;
#[derive(Properties, Default)]
#[properties(wrapper_type = super::ClientObject)]
pub struct ClientObject {
#[property(name = "handle", get, set, type = ClientHandle, member = handle)]
#[property(name = "hostname", get, set, type = String, member = hostname)]
#[property(name = "port", get, set, type = u32, member = port, maximum = u16::MAX as u32)]
#[property(name = "active", get, set, type = bool, member = active)]
#[property(name = "position", get, set, type = String, member = position)]
pub data: RefCell<ClientData>,
}
#[glib::object_subclass]
impl ObjectSubclass for ClientObject {
const NAME: &'static str = "ClientObject";
type Type = super::ClientObject;
}
#[glib::derived_properties]
impl ObjectImpl for ClientObject {}

View File

@@ -0,0 +1,119 @@
mod imp;
use adw::prelude::*;
use adw::subclass::prelude::*;
use gtk::glib::{self, Object};
use crate::config::DEFAULT_PORT;
use super::ClientObject;
glib::wrapper! {
pub struct ClientRow(ObjectSubclass<imp::ClientRow>)
@extends gtk::ListBoxRow, gtk::Widget, adw::PreferencesRow, adw::ExpanderRow,
@implements gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget;
}
impl ClientRow {
pub fn new(_client_object: &ClientObject) -> Self {
Object::builder().build()
}
pub fn bind(&self, client_object: &ClientObject) {
let mut bindings = self.imp().bindings.borrow_mut();
let active_binding = client_object
.bind_property("active", &self.imp().enable_switch.get(), "state")
.bidirectional()
.sync_create()
.build();
let hostname_binding = client_object
.bind_property("hostname", &self.imp().hostname.get(), "text")
.transform_to(|_, v: Option<String>| {
if let Some(hostname) = v {
Some(hostname)
} else {
Some("".to_string())
}
})
.transform_from(|_, v: String| {
if v.as_str().trim() == "" {
Some(None)
} else {
Some(Some(v))
}
})
.bidirectional()
.sync_create()
.build();
let title_binding = client_object
.bind_property("hostname", self, "title")
.transform_to(|_, v: Option<String>| {
if let Some(hostname) = v {
Some(hostname)
} else {
Some("<span font_style=\"italic\" font_weight=\"light\" foreground=\"darkgrey\">no hostname!</span>".to_string())
}
})
.sync_create()
.build();
let port_binding = client_object
.bind_property("port", &self.imp().port.get(), "text")
.transform_from(|_, v: String| {
if v.is_empty() {
Some(DEFAULT_PORT as u32)
} else {
Some(v.parse::<u16>().unwrap_or(DEFAULT_PORT) as u32)
}
})
.transform_to(|_, v: u32| {
if v == 4242 {
Some("".to_string())
} else {
Some(v.to_string())
}
})
.bidirectional()
.sync_create()
.build();
let subtitle_binding = client_object
.bind_property("port", self, "subtitle")
.sync_create()
.build();
let position_binding = client_object
.bind_property("position", &self.imp().position.get(), "selected")
.transform_from(|_, v: u32| match v {
1 => Some("right"),
2 => Some("top"),
3 => Some("bottom"),
_ => Some("left"),
})
.transform_to(|_, v: String| match v.as_str() {
"right" => Some(1),
"top" => Some(2u32),
"bottom" => Some(3u32),
_ => Some(0u32),
})
.bidirectional()
.sync_create()
.build();
bindings.push(active_binding);
bindings.push(hostname_binding);
bindings.push(title_binding);
bindings.push(port_binding);
bindings.push(subtitle_binding);
bindings.push(position_binding);
}
pub fn unbind(&self) {
for binding in self.imp().bindings.borrow_mut().drain(..) {
binding.unbind();
}
}
}

View File

@@ -0,0 +1,81 @@
use std::cell::RefCell;
use adw::subclass::prelude::*;
use adw::{prelude::*, ActionRow, ComboRow};
use glib::{subclass::InitializingObject, Binding};
use gtk::glib::clone;
use gtk::{glib, Button, CompositeTemplate, Switch};
#[derive(CompositeTemplate, Default)]
#[template(resource = "/de/feschber/LanMouse/client_row.ui")]
pub struct ClientRow {
#[template_child]
pub enable_switch: TemplateChild<gtk::Switch>,
#[template_child]
pub hostname: TemplateChild<gtk::Entry>,
#[template_child]
pub port: TemplateChild<gtk::Entry>,
#[template_child]
pub position: TemplateChild<ComboRow>,
#[template_child]
pub delete_row: TemplateChild<ActionRow>,
#[template_child]
pub delete_button: TemplateChild<gtk::Button>,
pub bindings: RefCell<Vec<Binding>>,
}
#[glib::object_subclass]
impl ObjectSubclass for ClientRow {
// `NAME` needs to match `class` attribute of template
const NAME: &'static str = "ClientRow";
type Type = super::ClientRow;
type ParentType = adw::ExpanderRow;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
klass.bind_template_callbacks();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for ClientRow {
fn constructed(&self) {
self.parent_constructed();
self.delete_button
.connect_clicked(clone!(@weak self as row => move |button| {
row.handle_client_delete(button);
}));
}
}
#[gtk::template_callbacks]
impl ClientRow {
#[template_callback]
fn handle_client_set_state(&self, state: bool, switch: &Switch) -> bool {
let idx = self.obj().index() as u32;
switch
.activate_action("win.request-client-update", Some(&idx.to_variant()))
.unwrap();
switch.set_state(state);
true // dont run default handler
}
#[template_callback]
fn handle_client_delete(&self, button: &Button) {
log::debug!("delete button pressed");
let idx = self.obj().index() as u32;
button
.activate_action("win.request-client-delete", Some(&idx.to_variant()))
.unwrap();
}
}
impl WidgetImpl for ClientRow {}
impl BoxImpl for ClientRow {}
impl ListBoxRowImpl for ClientRow {}
impl PreferencesRowImpl for ClientRow {}
impl ExpanderRowImpl for ClientRow {}

176
src/frontend/gtk/window.rs Normal file
View File

@@ -0,0 +1,176 @@
mod imp;
use std::io::Write;
use adw::prelude::*;
use adw::subclass::prelude::*;
use glib::{clone, Object};
use gtk::{gio, glib, NoSelection};
use crate::{
client::{ClientHandle, Position},
config::DEFAULT_PORT,
frontend::{gtk::client_object::ClientObject, FrontendEvent},
};
use super::client_row::ClientRow;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends adw::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pub(crate) fn new(app: &adw::Application) -> Self {
Object::builder().property("application", app).build()
}
pub fn clients(&self) -> gio::ListStore {
self.imp()
.clients
.borrow()
.clone()
.expect("Could not get clients")
}
fn setup_clients(&self) {
let model = gio::ListStore::new::<ClientObject>();
self.imp().clients.replace(Some(model));
let selection_model = NoSelection::new(Some(self.clients()));
self.imp().client_list.bind_model(
Some(&selection_model),
clone!(@weak self as window => @default-panic, move |obj| {
let client_object = obj.downcast_ref().expect("Expected object of type `ClientObject`.");
let row = window.create_client_row(client_object);
row.upcast()
})
);
}
/// workaround for a bug in libadwaita that shows an ugly line beneath
/// the last element if a placeholder is set.
/// https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6308
pub fn set_placeholder_visible(&self, visible: bool) {
let placeholder = self.imp().client_placeholder.get();
self.imp().client_list.set_placeholder(match visible {
true => Some(&placeholder),
false => None,
});
}
fn setup_icon(&self) {
self.set_icon_name(Some("mouse-icon"));
}
fn create_client_row(&self, client_object: &ClientObject) -> ClientRow {
let row = ClientRow::new(client_object);
row.bind(client_object);
row
}
pub fn new_client(
&self,
handle: ClientHandle,
hostname: Option<String>,
port: u16,
position: Position,
active: bool,
) {
let client = ClientObject::new(handle, hostname, port as u32, position.to_string(), active);
self.clients().append(&client);
self.set_placeholder_visible(false);
}
pub fn client_idx(&self, handle: ClientHandle) -> Option<usize> {
self.clients().iter::<ClientObject>().position(|c| {
if let Ok(c) = c {
c.handle() == handle
} else {
false
}
})
}
pub fn delete_client(&self, handle: ClientHandle) {
let Some(idx) = self.client_idx(handle) else {
log::warn!("could not find client with handle {handle}");
return;
};
self.clients().remove(idx as u32);
if self.clients().n_items() == 0 {
self.set_placeholder_visible(true);
}
}
pub fn request_client_create(&self) {
let event = FrontendEvent::AddClient(None, DEFAULT_PORT, Position::default());
self.imp().set_port(DEFAULT_PORT);
self.request(event);
}
pub fn request_port_change(&self) {
let port = self.imp().port_entry.get().text().to_string();
if let Ok(port) = port.as_str().parse::<u16>() {
self.request(FrontendEvent::ChangePort(port));
} else {
self.request(FrontendEvent::ChangePort(DEFAULT_PORT));
}
}
pub fn request_client_update(&self, client: &ClientObject) {
let data = client.get_data();
let position = match data.position.as_str() {
"left" => Position::Left,
"right" => Position::Right,
"top" => Position::Top,
"bottom" => Position::Bottom,
_ => {
log::error!("invalid position: {}", data.position);
return;
}
};
let hostname = data.hostname;
let port = data.port as u16;
let event = FrontendEvent::UpdateClient(client.handle(), hostname, port, position);
self.request(event);
let event = FrontendEvent::ActivateClient(client.handle(), !client.active());
self.request(event);
}
pub fn request_client_delete(&self, idx: u32) {
if let Some(obj) = self.clients().item(idx) {
let client_object: &ClientObject = obj
.downcast_ref()
.expect("Expected object of type `ClientObject`.");
let handle = client_object.handle();
let event = FrontendEvent::DelClient(handle);
self.request(event);
}
}
fn request(&self, event: FrontendEvent) {
let json = serde_json::to_string(&event).unwrap();
log::debug!("requesting {json}");
let mut stream = self.imp().stream.borrow_mut();
let stream = stream.as_mut().unwrap();
let bytes = json.as_bytes();
let len = bytes.len().to_be_bytes();
if let Err(e) = stream.write(&len) {
log::error!("error sending message: {e}");
};
if let Err(e) = stream.write(bytes) {
log::error!("error sending message: {e}");
};
}
pub fn show_toast(&self, msg: &str) {
let toast = adw::Toast::new(msg);
let toast_overlay = &self.imp().toast_overlay;
toast_overlay.add_toast(toast);
}
}

View File

@@ -0,0 +1,110 @@
use std::cell::{Cell, RefCell};
#[cfg(windows)]
use std::net::TcpStream;
#[cfg(unix)]
use std::os::unix::net::UnixStream;
use adw::subclass::prelude::*;
use adw::{
prelude::{EditableExt, WidgetExt},
ActionRow, ToastOverlay,
};
use glib::subclass::InitializingObject;
use gtk::{gio, glib, Button, CompositeTemplate, Entry, ListBox};
use crate::config::DEFAULT_PORT;
#[derive(CompositeTemplate, Default)]
#[template(resource = "/de/feschber/LanMouse/window.ui")]
pub struct Window {
#[template_child]
pub port_edit_apply: TemplateChild<Button>,
#[template_child]
pub port_edit_cancel: TemplateChild<Button>,
#[template_child]
pub client_list: TemplateChild<ListBox>,
#[template_child]
pub client_placeholder: TemplateChild<ActionRow>,
#[template_child]
pub port_entry: TemplateChild<Entry>,
#[template_child]
pub toast_overlay: TemplateChild<ToastOverlay>,
pub clients: RefCell<Option<gio::ListStore>>,
#[cfg(unix)]
pub stream: RefCell<Option<UnixStream>>,
#[cfg(windows)]
pub stream: RefCell<Option<TcpStream>>,
pub port: Cell<u16>,
}
#[glib::object_subclass]
impl ObjectSubclass for Window {
// `NAME` needs to match `class` attribute of template
const NAME: &'static str = "LanMouseWindow";
type Type = super::Window;
type ParentType = adw::ApplicationWindow;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
klass.bind_template_callbacks();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[gtk::template_callbacks]
impl Window {
#[template_callback]
fn handle_add_client_pressed(&self, _button: &Button) {
self.obj().request_client_create();
}
#[template_callback]
fn handle_port_changed(&self, _entry: &Entry) {
self.port_edit_apply.set_visible(true);
self.port_edit_cancel.set_visible(true);
}
#[template_callback]
fn handle_port_edit_apply(&self) {
self.obj().request_port_change();
}
#[template_callback]
fn handle_port_edit_cancel(&self) {
log::debug!("cancel port edit");
self.port_entry
.set_text(self.port.get().to_string().as_str());
self.port_edit_apply.set_visible(false);
self.port_edit_cancel.set_visible(false);
}
pub fn set_port(&self, port: u16) {
self.port.set(port);
if port == DEFAULT_PORT {
self.port_entry.set_text("");
} else {
self.port_entry.set_text(format!("{port}").as_str());
}
self.port_edit_apply.set_visible(false);
self.port_edit_cancel.set_visible(false);
}
}
impl ObjectImpl for Window {
fn constructed(&self) {
self.parent_constructed();
self.set_port(DEFAULT_PORT);
let obj = self.obj();
obj.setup_icon();
obj.setup_clients();
}
}
impl WidgetImpl for Window {}
impl WindowImpl for Window {}
impl ApplicationWindowImpl for Window {}
impl AdwApplicationWindowImpl for Window {}

View File

@@ -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)
}

View File

@@ -2,10 +2,11 @@ pub mod client;
pub mod config;
pub mod dns;
pub mod event;
pub mod request;
pub mod server;
pub mod consumer;
pub mod producer;
pub mod backend;
pub mod ioutils;
pub mod frontend;
pub mod scancode;

View File

@@ -1,111 +1,74 @@
use std::{sync::{mpsc, Arc}, process, env};
use anyhow::Result;
use std::process::{self, Child, Command};
use lan_mouse::{
client::ClientManager,
consumer, producer,
config, event, request,
};
use env_logger::Env;
use lan_mouse::{config::Config, frontend, server::Server};
fn usage() {
eprintln!("usage: {} [--backend <backend>] [--port <port>]",
env::args().next().unwrap_or("lan-mouse".into()));
}
use tokio::task::LocalSet;
pub fn main() {
// parse config file
let config = match config::Config::new() {
Err(e) => {
eprintln!("{e}");
usage();
process::exit(1);
}
Ok(config) => config,
};
// init logging
let env = Env::default().filter_or("LAN_MOUSE_LOG_LEVEL", "info");
env_logger::init_from_env(env);
// port or default
let port = config.port;
// event channel for producing events
let (produce_tx, produce_rx) = mpsc::sync_channel(128);
// event channel for consuming events
let (consume_tx, consume_rx) = mpsc::sync_channel(128);
// create client manager
let client_manager = match ClientManager::new(&config) {
Err(e) => {
eprintln!("{e}");
process::exit(1);
}
Ok(m) => m,
};
// start receiving client connection requests
let (request_server, request_thread) = match request::Server::listen(port) {
Err(e) => {
eprintln!("Could not bind to port {port}: {e}");
process::exit(1);
}
Ok(r) => r,
};
println!("Press Ctrl+Alt+Shift+Super to release the mouse");
// start producing and consuming events
let event_producer = match producer::start(produce_tx, client_manager.get_clients(), request_server) {
Err(e) => {
eprintln!("Could not start event producer: {e}");
None
},
Ok(p) => Some(p),
};
let event_consumer = match consumer::start(consume_rx, client_manager.get_clients(), config.backend) {
Err(e) => {
eprintln!("Could not start event consumer: {e}");
None
},
Ok(p) => Some(p),
};
if event_consumer.is_none() && event_producer.is_none() {
process::exit(1);
}
// start sending and receiving events
let event_server = match event::server::Server::new(port) {
Ok(s) => s,
Err(e) => {
eprintln!("{e}");
process::exit(1);
}
};
let (receiver, sender) = match event_server.run(Arc::new(client_manager), produce_rx, consume_tx) {
Ok((r,s)) => (r,s),
Err(e) => {
eprintln!("{e}");
process::exit(1);
}
};
request_thread.join().unwrap();
// stop receiving events and terminate event-consumer
if let Err(e) = receiver.join().unwrap() {
eprint!("{e}");
process::exit(1);
}
if let Some(thread) = event_consumer {
thread.join().unwrap();
}
// stop producing events and terminate event-sender
if let Some(thread) = event_producer {
thread.join().unwrap();
}
if let Err(e) = sender.join().unwrap() {
eprint!("{e}");
if let Err(e) = run() {
log::error!("{e}");
process::exit(1);
}
}
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:?}");
if config.daemon {
// if daemon is specified we run the service
run_service(&config)?;
} else {
// otherwise start the service as a child process and
// run a frontend
let mut service = start_service()?;
frontend::run_frontend(&config)?;
log::info!("terminating service");
#[cfg(unix)]
{
// on unix we give the service a chance to terminate gracefully
let pid = service.id() as libc::pid_t;
unsafe {
libc::kill(pid, libc::SIGINT);
}
service.wait()?;
}
service.kill()?;
}
anyhow::Ok(())
}
fn run_service(config: &Config) -> Result<()> {
// create single threaded tokio runtime
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()?;
// run async event loop
runtime.block_on(LocalSet::new().run_until(async {
// run main loop
log::info!("Press Ctrl+Alt+Shift+Super to release the mouse");
Server::run(config).await?;
log::debug!("service exiting");
anyhow::Ok(())
}))?;
Ok(())
}

View File

@@ -1,52 +1,61 @@
#[cfg(unix)]
use std::env;
use std::{thread::{JoinHandle, self}, sync::mpsc::SyncSender, error::Error};
use std::io;
use crate::{client::{Client, ClientHandle}, event::Event, request::Server};
use futures_core::Stream;
use crate::backend::producer;
use crate::{
client::{ClientEvent, ClientHandle},
event::Event,
};
#[cfg(unix)]
enum Backend {
Wayland,
X11,
pub async fn create() -> Box<dyn EventProducer> {
#[cfg(target_os = "macos")]
match producer::macos::MacOSProducer::new() {
Ok(p) => return Box::new(p),
Err(e) => log::info!("macos event producer not available: {e}"),
}
#[cfg(windows)]
match producer::windows::WindowsProducer::new() {
Ok(p) => return Box::new(p),
Err(e) => log::info!("windows event producer not available: {e}"),
}
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
match producer::libei::LibeiProducer::new() {
Ok(p) => {
log::info!("using libei event producer");
return Box::new(p);
}
Err(e) => log::info!("libei event producer not available: {e}"),
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
match producer::wayland::WaylandEventProducer::new() {
Ok(p) => {
log::info!("using layer-shell event producer");
return Box::new(p);
}
Err(e) => log::info!("layer_shell event producer not available: {e}"),
}
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
match producer::x11::X11Producer::new() {
Ok(p) => {
log::info!("using x11 event producer");
return Box::new(p);
}
Err(e) => log::info!("x11 event producer not available: {e}"),
}
log::error!("falling back to dummy event producer");
Box::new(producer::dummy::DummyProducer::new())
}
pub fn start(
produce_tx: SyncSender<(Event, ClientHandle)>,
clients: Vec<Client>,
request_server: Server,
) -> Result<JoinHandle<()>, Box<dyn Error>> {
Ok(thread::Builder::new()
.name("event producer".into())
.spawn(move || {
#[cfg(windows)]
producer::windows::run(produce_tx, request_server, clients);
pub trait EventProducer: Stream<Item = io::Result<(ClientHandle, Event)>> + Unpin {
/// notify event producer of configuration changes
fn notify(&mut self, event: ClientEvent);
#[cfg(unix)]
let backend = match env::var("XDG_SESSION_TYPE") {
Ok(session_type) => match session_type.as_str() {
"x11" => Backend::X11,
"wayland" => 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")]
producer::x11::run(produce_tx, request_server, clients);
}
Backend::Wayland => {
#[cfg(not(feature = "wayland"))]
panic!("feature wayland not enabled");
#[cfg(feature = "wayland")]
producer::wayland::run(produce_tx, request_server, clients);
}
}
})?)
/// release mouse
fn release(&mut self);
}

View File

@@ -1,139 +0,0 @@
use std::{
collections::HashMap,
error::Error,
fmt::Display,
io::prelude::*,
net::{SocketAddr, TcpListener, TcpStream},
sync::{Arc, RwLock},
thread::{self, JoinHandle},
};
use memmap::MmapMut;
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub enum Request {
KeyMap,
Connect,
}
impl TryFrom<[u8; 4]> for Request {
fn try_from(buf: [u8; 4]) -> Result<Self, Self::Error> {
let val = u32::from_ne_bytes(buf);
match val {
x if x == Request::KeyMap as u32 => Ok(Self::KeyMap),
x if x == Request::Connect as u32 => Ok(Self::Connect),
_ => Err("Bad Request"),
}
}
type Error = &'static str;
}
#[derive(Clone)]
pub struct Server {
data: Arc<RwLock<HashMap<Request, MmapMut>>>,
}
impl Server {
fn handle_request(&self, mut stream: TcpStream) -> Result<(), Box<dyn Error>> {
let mut buf = [0u8; 4];
stream.read_exact(&mut buf)?;
match Request::try_from(buf) {
Ok(Request::KeyMap) => {
let data = self.data.read().unwrap();
let buf = data.get(&Request::KeyMap);
match buf {
None => {
stream.write(&0u32.to_ne_bytes())?;
}
Some(buf) => {
stream.write(&buf[..].len().to_ne_bytes())?;
stream.write(&buf[..])?;
}
}
stream.flush()?;
}
Ok(Request::Connect) => todo!(),
Err(msg) => eprintln!("{}", msg),
}
Ok(())
}
pub fn listen(port: u16) -> Result<(Server, JoinHandle<()>), Box<dyn Error>> {
let data: Arc<RwLock<HashMap<Request, MmapMut>>> = Arc::new(RwLock::new(HashMap::new()));
let listen_addr = SocketAddr::new("0.0.0.0".parse()?, port);
let server = Server { data };
let server_copy = server.clone();
let listen_socket = TcpListener::bind(listen_addr)?;
let thread = thread::Builder::new()
.name("tcp server".into())
.spawn(move || {
for stream in listen_socket.incoming() {
match stream {
Ok(stream) => {
if let Err(e) = server.handle_request(stream) {
eprintln!("{}", e);
}
}
Err(e) => {
eprintln!("{}", e);
}
}
}
})?;
Ok((server_copy, thread))
}
pub fn offer_data(&self, req: Request, d: MmapMut) {
self.data.write().unwrap().insert(req, d);
}
}
#[derive(Debug)]
pub struct BadRequest;
impl Display for BadRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "BadRequest")
}
}
impl Error for BadRequest {}
pub fn request_data(addr: SocketAddr, req: Request) -> Result<Vec<u8>, Box<dyn Error>> {
// connect to server
let mut sock = match TcpStream::connect(addr) {
Ok(sock) => sock,
Err(e) => return Err(Box::new(e)),
};
// write the request to the socket
// convert to u32
let req: u32 = req as u32;
if let Err(e) = sock.write(&req.to_ne_bytes()) {
return Err(Box::new(e));
}
if let Err(e) = sock.flush() {
return Err(Box::new(e));
}
// read the response = (len, data) - len 0 means no data / bad request
// read len
let mut buf = [0u8; 8];
if let Err(e) = sock.read_exact(&mut buf[..]) {
return Err(Box::new(e));
}
let len = usize::from_ne_bytes(buf);
// check for bad request
if len == 0 {
return Err(Box::new(BadRequest {}));
}
// read the data
let mut data: Vec<u8> = vec![0u8; len];
if let Err(e) = sock.read_exact(&mut data[..]) {
return Err(Box::new(e));
}
Ok(data)
}

698
src/scancode.rs Normal file
View File

@@ -0,0 +1,698 @@
/*
* https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input
*/
#[repr(u32)]
#[derive(Debug, Clone, Copy)]
pub enum Windows {
Shutdown = 0xE05E,
SystemSleep = 0xE05F,
SystemWakeUp = 0xE063,
ErrorRollOver = 0x00FF,
KeyA = 0x001E,
KeyB = 0x0030,
KeyC = 0x002E,
KeyD = 0x0020,
KeyE = 0x0012,
KeyF = 0x0021,
KeyG = 0x0022,
KeyH = 0x0023,
KeyI = 0x0017,
KeyJ = 0x0024,
KeyK = 0x0025,
KeyL = 0x0026,
KeyM = 0x0032,
KeyN = 0x0031,
KeyO = 0x0018,
KeyP = 0x0019,
KeyQ = 0x0010,
KeyR = 0x0013,
KeyS = 0x001F,
KeyT = 0x0014,
KeyU = 0x0016,
KeyV = 0x002F,
KeyW = 0x0011,
KeyX = 0x002D,
KeyY = 0x0015,
KeyZ = 0x002C,
Key1 = 0x0002,
Key2 = 0x0003,
Key3 = 0x0004,
Key4 = 0x0005,
Key5 = 0x0006,
Key6 = 0x0007,
Key7 = 0x0008,
Key8 = 0x0009,
Key9 = 0x000A,
Key0 = 0x000B,
KeyEnter = 0x001C,
KeyEsc = 0x0001,
KeyDelete = 0x000E,
KeyTab = 0x000F,
KeySpace = 0x0039,
KeyMinus = 0x000C,
KeyEqual = 0x000D,
KeyLeftBrace = 0x001A,
KeyRightBrace = 0x001B,
KeyBackslash = 0x002B,
KeySemiColon = 0x0027,
KeyApostrophe = 0x0028,
KeyGrave = 0x0029,
KeyComma = 0x0033,
KeyDot = 0x0034,
KeySlash = 0x0035,
KeyCapsLock = 0x003A,
KeyF1 = 0x003B,
KeyF2 = 0x003C,
KeyF3 = 0x003D,
KeyF4 = 0x003E,
KeyF5 = 0x003F,
KeyF6 = 0x0040,
KeyF7 = 0x0041,
KeyF8 = 0x0042,
KeyF9 = 0x0043,
KeyF10 = 0x0044,
KeyF11 = 0x0057,
KeyF12 = 0x0058,
KeyPrintScreen = 0xE037,
KeyScrollLock = 0x0046,
KeyPause = 0xE11D45,
KeyInsert = 0xE052,
KeyHome = 0xE047,
KeyPageUp = 0xE049,
KeyDeleteForward = 0xE053,
KeyEnd = 0xE04F,
KeyPageDown = 0xE051,
KeyRight = 0xE04D,
KeyLeft = 0xE04B,
KeyDown = 0xE050,
KeyUp = 0xE048,
KeypadNumLock = 0x0045,
KeypadSlash = 0xE035,
KeypadStar = 0x0037,
KeypadDash = 0x004A,
KeypadPlus = 0x004E,
KeypadEnter = 0xE01C,
Keypad1End = 0x004F,
Keypad2DownArrow = 0x0050,
Keypad3PageDn = 0x0051,
Keypad4LeftArrow = 0x004B,
Keypad5 = 0x004C,
Keypad6RightArrow = 0x004D,
Keypad7Home = 0x0047,
Keypad8UpArrow = 0x0048,
Keypad9PageUp = 0x0049,
Keypad0Insert = 0x0052,
KeypadDot = 0x0053,
KeyNonUSSlashBar = 0x0056,
KeyApplication = 0xE05D,
KeypadEquals = 0x0059,
KeyF13 = 0x0064,
KeyF14 = 0x0065,
KeyF15 = 0x0066,
KeyF16 = 0x0067,
KeyF17 = 0x0068,
KeyF18 = 0x0069,
KeyF19 = 0x006A,
KeyF20 = 0x006B,
KeyF21 = 0x006C,
KeyF22 = 0x006D,
KeyF23 = 0x006E,
KeyF24 = 0x0076,
KeypadComma = 0x007E,
KeyInternational1 = 0x0073,
KeyInternational2 = 0x0070,
KeyInternational3 = 0x007D,
#[allow(dead_code)]
KeyInternational4 = 0x0079, // FIXME unused
#[allow(dead_code)]
KeyInternational5 = 0x007B, // FIXME unused
// KeyInternational6 = 0x005C,
KeyLANG1 = 0x0072,
KeyLANG2 = 0x0071,
KeyLANG3 = 0x0078,
KeyLANG4 = 0x0077,
// KeyLANG5 = 0x0076,
KeyLeftCtrl = 0x001D,
KeyLeftShift = 0x002A,
KeyLeftAlt = 0x0038,
KeyLeftGUI = 0xE05B,
KeyRightCtrl = 0xE01D,
KeyRightShift = 0x0036,
KeyRightAlt = 0xE038,
KeyRightGUI = 0xE05C,
KeyScanNextTrack = 0xE019,
KeyScanPreviousTrack = 0xE010,
KeyStop = 0xE024,
KeyPlayPause = 0xE022,
KeyMute = 0xE020,
KeyVolumeUp = 0xE030,
KeyVolumeDown = 0xE02E,
#[allow(dead_code)]
ALConsumerControlConfiguration = 0xE06D, // TODO Unused
ALEmailReader = 0xE06C,
ALCalculator = 0xE021,
ALLocalMachineBrowser = 0xE06B,
ACSearch = 0xE065,
ACHome = 0xE032,
ACBack = 0xE06A,
ACForward = 0xE069,
ACStop = 0xE068,
ACRefresh = 0xE067,
ACBookmarks = 0xE066,
}
/*
* https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h
*/
#[repr(u32)]
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub enum Linux {
KeyReserved = 0,
KeyEsc = 1,
Key1 = 2,
Key2 = 3,
Key3 = 4,
Key4 = 5,
Key5 = 6,
Key6 = 7,
Key7 = 8,
Key8 = 9,
Key9 = 10,
Key0 = 11,
KeyMinus = 12,
KeyEqual = 13,
KeyBackspace = 14,
KeyTab = 15,
KeyQ = 16,
KeyW = 17,
KeyE = 18,
KeyR = 19,
KeyT = 20,
KeyY = 21,
KeyU = 22,
KeyI = 23,
KeyO = 24,
KeyP = 25,
KeyLeftbrace = 26,
KeyRightbrace = 27,
KeyEnter = 28,
KeyLeftCtrl = 29,
KeyA = 30,
KeyS = 31,
KeyD = 32,
KeyF = 33,
KeyG = 34,
KeyH = 35,
KeyJ = 36,
KeyK = 37,
KeyL = 38,
KeySemicolon = 39,
KeyApostrophe = 40,
KeyGrave = 41,
KeyLeftshift = 42,
KeyBackslash = 43,
KeyZ = 44,
KeyX = 45,
KeyC = 46,
KeyV = 47,
KeyB = 48,
KeyN = 49,
KeyM = 50,
KeyComma = 51,
KeyDot = 52,
KeySlash = 53,
KeyRightShift = 54,
KeyKpAsterisk = 55,
KeyLeftalt = 56,
KeySpace = 57,
KeyCapsLock = 58,
KeyF1 = 59,
KeyF2 = 60,
KeyF3 = 61,
KeyF4 = 62,
KeyF5 = 63,
KeyF6 = 64,
KeyF7 = 65,
KeyF8 = 66,
KeyF9 = 67,
KeyF10 = 68,
KeyNumlock = 69,
KeyScrollLock = 70,
KeyKp7 = 71,
KeyKp8 = 72,
KeyKp9 = 73,
KeyKpMinus = 74,
KeyKp4 = 75,
KeyKp5 = 76,
KeyKp6 = 77,
KeyKpplus = 78,
KeyKp1 = 79,
KeyKp2 = 80,
KeyKp3 = 81,
KeyKp0 = 82,
KeyKpDot = 83,
Invalid = 84,
KeyZenkakuhankaku = 85,
Key102nd = 86,
KeyF11 = 87,
KeyF12 = 88,
KeyRo = 89,
KeyKatakana = 90,
KeyHiragana = 91,
KeyHenkan = 92,
KeyKatakanahiragana = 93,
KeyMuhenkan = 94,
KeyKpJpComma = 95,
KeyKpEnter = 96,
KeyRightCtrl = 97,
KeyKpslash = 98,
KeySysrq = 99,
KeyRightalt = 100,
KeyLinefeed = 101,
KeyHome = 102,
KeyUp = 103,
KeyPageup = 104,
KeyLeft = 105,
KeyRight = 106,
KeyEnd = 107,
KeyDown = 108,
KeyPagedown = 109,
KeyInsert = 110,
KeyDelete = 111,
KeyMacro = 112,
KeyMute = 113,
KeyVolumeDown = 114,
KeyVolumeUp = 115,
KeyPower = 116, /* SC System Power Down */
KeyKpequal = 117,
KeyKpplusminus = 118,
KeyPause = 119,
KeyScale = 120, /* AL Compiz Scale (Expose) */
KeyKpcomma = 121,
KeyHangeul = 122,
// KEY_HANGUEL = KeyHangeul,
KeyHanja = 123,
KeyYen = 124,
KeyLeftmeta = 125,
KeyRightmeta = 126,
KeyCompose = 127,
KeyStop = 128, /* AC Stop */
KeyAgain = 129,
KeyProps = 130, /* AC Properties */
KeyUndo = 131, /* AC Undo */
KeyFront = 132,
KeyCopy = 133, /* AC Copy */
KeyOpen = 134, /* AC Open */
KeyPaste = 135, /* AC Paste */
KeyFind = 136, /* AC Search */
KeyCut = 137, /* AC Cut */
KeyHelp = 138, /* AL Integrated Help Center */
KeyMenu = 139, /* Menu (show menu) */
KeyCalc = 140, /* AL Calculator */
KeySetup = 141,
KeySleep = 142, /* SC System Sleep */
KeyWakeup = 143, /* System Wake Up */
KeyFile = 144, /* AL Local Machine Browser */
KeySendfile = 145,
KeyDeletefile = 146,
KeyXfer = 147,
KeyProg1 = 148,
KeyProg2 = 149,
KeyWww = 150, /* AL Internet Browser */
KeyMsdos = 151,
KeyCoffee = 152, /* AL Terminal Lock/Screensaver */
// KEY_SCREENLOCK = KeyCoffee,
KeyRotateDisplay = 153, /* Display orientation for e.g. tablets */
// KEY_DIRECTION = KeyRotateDisplay,
KeyCyclewindows = 154,
KeyMail = 155,
KeyBookmarks = 156, /* AC Bookmarks */
KeyComputer = 157,
KeyBack = 158, /* AC Back */
KeyForward = 159, /* AC Forward */
KeyClosecd = 160,
KeyEjectcd = 161,
KeyEjectclosecd = 162,
KeyNextsong = 163,
KeyPlaypause = 164,
KeyPrevioussong = 165,
KeyStopcd = 166,
KeyRecord = 167,
KeyRewind = 168,
KeyPhone = 169, /* Media Select Telephone */
KeyIso = 170,
KeyConfig = 171, /* AL Consumer Control Configuration */
KeyHomepage = 172, /* AC Home */
KeyRefresh = 173, /* AC Refresh */
KeyExit = 174, /* AC Exit */
KeyMove = 175,
KeyEdit = 176,
KeyScrollup = 177,
KeyScrolldown = 178,
KeyKpleftparen = 179,
KeyKprightparen = 180,
KeyNew = 181, /* AC New */
KeyRedo = 182, /* AC Redo/Repeat */
KeyF13 = 183,
KeyF14 = 184,
KeyF15 = 185,
KeyF16 = 186,
KeyF17 = 187,
KeyF18 = 188,
KeyF19 = 189,
KeyF20 = 190,
KeyF21 = 191,
KeyF22 = 192,
KeyF23 = 193,
KeyF24 = 194,
Invalid1 = 195,
Invalid2 = 196,
Invalid3 = 197,
Invalid4 = 198,
Invalid5 = 199,
KeyPlaycd = 200,
KeyPausecd = 201,
KeyProg3 = 202,
KeyProg4 = 203,
KeyAllApplications = 204, /* AC Desktop Show All Applications */
// KEY_DASHBOARD = KeyAllApplications,
KeySuspend = 205,
KeyClose = 206, /* AC Close */
KeyPlay = 207,
KeyFastforward = 208,
KeyBassboost = 209,
KeyPrint = 210, /* AC Print */
KeyHp = 211,
KeyCamera = 212,
KeySound = 213,
KeyQuestion = 214,
KeyEmail = 215,
KeyChat = 216,
KeySearch = 217,
KeyConnect = 218,
KeyFinance = 219, /* AL Checkbook/Finance */
KeySport = 220,
KeyShop = 221,
KeyAlterase = 222,
KeyCancel = 223, /* AC Cancel */
KeyBrightnessdown = 224,
KeyBrightnessup = 225,
KeyMedia = 226,
KeySwitchvideomode = 227, /* Cycle between available video, outputs (Monitor/LCD/TV-out/etc) */
KeyKbdillumtoggle = 228,
KeyKbdillumdown = 229,
KeyKbdillumup = 230,
KeySend = 231, /* AC Send */
KeyReply = 232, /* AC Reply */
KeyForwardmail = 233, /* AC Forward Msg */
KeySave = 234, /* AC Save */
KeyDocuments = 235,
KeyBattery = 236,
KeyBluetooth = 237,
KeyWlan = 238,
KeyUwb = 239,
KeyUnknown = 240,
KeyVideoNext = 241, /* drive next video source */
KeyVideoPrev = 242, /* drive previous video source */
KeyBrightnessCycle = 243, /* brightness up, after max is min */
KeyBrightnessAuto = 244, /* Set Auto Brightness: manual, brightness control is off, rely on ambient */
// KEY_BRIGHTNESS_ZERO=KeyBrightnessAuto,
KeyDisplayOff = 245, /* display device to off state */
KeyWwan = 246, /* Wireless WAN (LTE, UMTS, GSM, etc.) */
// KEY_WIMAX = KeyWwan,
KeyRfkill = 247, /* Key that controls all radios */
KeyMicmute = 248, /* Mute / unmute the microphone */
KeyCount = 249,
}
impl TryFrom<u32> for Linux {
type Error = ();
fn try_from(value: u32) -> Result<Self, Self::Error> {
if value >= Self::KeyCount as u32 {
return Err(());
}
let code: Linux = unsafe { std::mem::transmute(value) };
Ok(code)
}
}
impl TryFrom<Linux> for Windows {
type Error = ();
fn try_from(value: Linux) -> Result<Self, Self::Error> {
match value {
Linux::KeyReserved => Err(()),
Linux::KeyEsc => Ok(Self::KeyEsc),
Linux::Key1 => Ok(Self::Key1),
Linux::Key2 => Ok(Self::Key2),
Linux::Key3 => Ok(Self::Key3),
Linux::Key4 => Ok(Self::Key4),
Linux::Key5 => Ok(Self::Key5),
Linux::Key6 => Ok(Self::Key6),
Linux::Key7 => Ok(Self::Key7),
Linux::Key8 => Ok(Self::Key8),
Linux::Key9 => Ok(Self::Key9),
Linux::Key0 => Ok(Self::Key0),
Linux::KeyMinus => Ok(Self::KeyMinus),
Linux::KeyEqual => Ok(Self::KeyEqual),
Linux::KeyBackspace => Ok(Self::KeyDelete),
Linux::KeyTab => Ok(Self::KeyTab),
Linux::KeyQ => Ok(Self::KeyQ),
Linux::KeyW => Ok(Self::KeyW),
Linux::KeyE => Ok(Self::KeyE),
Linux::KeyR => Ok(Self::KeyR),
Linux::KeyT => Ok(Self::KeyT),
Linux::KeyY => Ok(Self::KeyY),
Linux::KeyU => Ok(Self::KeyU),
Linux::KeyI => Ok(Self::KeyI),
Linux::KeyO => Ok(Self::KeyO),
Linux::KeyP => Ok(Self::KeyP),
Linux::KeyLeftbrace => Ok(Self::KeyLeftBrace),
Linux::KeyRightbrace => Ok(Self::KeyRightBrace),
Linux::KeyEnter => Ok(Self::KeyEnter),
Linux::KeyLeftCtrl => Ok(Self::KeyLeftCtrl),
Linux::KeyA => Ok(Self::KeyA),
Linux::KeyS => Ok(Self::KeyS),
Linux::KeyD => Ok(Self::KeyD),
Linux::KeyF => Ok(Self::KeyF),
Linux::KeyG => Ok(Self::KeyG),
Linux::KeyH => Ok(Self::KeyH),
Linux::KeyJ => Ok(Self::KeyJ),
Linux::KeyK => Ok(Self::KeyK),
Linux::KeyL => Ok(Self::KeyL),
Linux::KeySemicolon => Ok(Self::KeySemiColon),
Linux::KeyApostrophe => Ok(Self::KeyApostrophe),
Linux::KeyGrave => Ok(Self::KeyGrave),
Linux::KeyLeftshift => Ok(Self::KeyLeftShift),
Linux::KeyBackslash => Ok(Self::KeyBackslash),
Linux::KeyZ => Ok(Self::KeyZ),
Linux::KeyX => Ok(Self::KeyX),
Linux::KeyC => Ok(Self::KeyC),
Linux::KeyV => Ok(Self::KeyV),
Linux::KeyB => Ok(Self::KeyB),
Linux::KeyN => Ok(Self::KeyN),
Linux::KeyM => Ok(Self::KeyM),
Linux::KeyComma => Ok(Self::KeyComma),
Linux::KeyDot => Ok(Self::KeyDot),
Linux::KeySlash => Ok(Self::KeySlash),
Linux::KeyRightShift => Ok(Self::KeyRightShift),
Linux::KeyKpAsterisk => Ok(Self::KeypadStar),
Linux::KeyLeftalt => Ok(Self::KeyLeftAlt),
Linux::KeySpace => Ok(Self::KeySpace),
Linux::KeyCapsLock => Ok(Self::KeyCapsLock),
Linux::KeyF1 => Ok(Self::KeyF1),
Linux::KeyF2 => Ok(Self::KeyF2),
Linux::KeyF3 => Ok(Self::KeyF3),
Linux::KeyF4 => Ok(Self::KeyF4),
Linux::KeyF5 => Ok(Self::KeyF5),
Linux::KeyF6 => Ok(Self::KeyF6),
Linux::KeyF7 => Ok(Self::KeyF7),
Linux::KeyF8 => Ok(Self::KeyF8),
Linux::KeyF9 => Ok(Self::KeyF9),
Linux::KeyF10 => Ok(Self::KeyF10),
Linux::KeyNumlock => Ok(Self::KeypadNumLock),
Linux::KeyScrollLock => Ok(Self::KeyScrollLock),
Linux::KeyKp7 => Ok(Self::Keypad7Home),
Linux::KeyKp8 => Ok(Self::Keypad8UpArrow),
Linux::KeyKp9 => Ok(Self::Keypad9PageUp),
Linux::KeyKpMinus => Ok(Self::KeypadDash),
Linux::KeyKp4 => Ok(Self::Keypad4LeftArrow),
Linux::KeyKp5 => Ok(Self::Keypad5),
Linux::KeyKp6 => Ok(Self::Keypad6RightArrow),
Linux::KeyKpplus => Ok(Self::KeypadPlus),
Linux::KeyKp1 => Ok(Self::Keypad1End),
Linux::KeyKp2 => Ok(Self::Keypad2DownArrow),
Linux::KeyKp3 => Ok(Self::Keypad3PageDn),
Linux::KeyKp0 => Ok(Self::Keypad0Insert),
Linux::KeyKpDot => Ok(Self::KeypadDot),
Linux::KeyZenkakuhankaku => Ok(Self::KeyLANG1), // TODO unsure
Linux::Key102nd => Ok(Self::KeyNonUSSlashBar), // TODO unsure
Linux::KeyF11 => Ok(Self::KeyF11),
Linux::KeyF12 => Ok(Self::KeyF12),
Linux::KeyRo => Ok(Self::ErrorRollOver), // TODO unsure
Linux::KeyKatakana => Ok(Self::KeyLANG1), // TODO unsure
Linux::KeyHiragana => Ok(Self::KeyLANG2), // TODO unsure
Linux::KeyHenkan => Ok(Self::KeyLANG3), // TODO unsure
Linux::KeyKatakanahiragana => Ok(Self::KeyLANG4), // TODO unsure
Linux::KeyMuhenkan => Ok(Self::KeyLANG4), // TODO unsure
Linux::KeyKpJpComma => Ok(Self::KeypadComma),
Linux::KeyKpEnter => Ok(Self::KeypadEnter),
Linux::KeyRightCtrl => Ok(Self::KeyRightCtrl),
Linux::KeyKpslash => Ok(Self::KeypadSlash),
Linux::KeySysrq => Ok(Self::KeyPrintScreen), // TODO Windows does not have Sysrq, right?
Linux::KeyRightalt => Ok(Self::KeyRightAlt),
Linux::KeyLinefeed => Ok(Self::KeyEnter), // TODO unsure
Linux::KeyHome => Ok(Self::KeyHome),
Linux::KeyUp => Ok(Self::KeyUp),
Linux::KeyPageup => Ok(Self::KeyPageUp),
Linux::KeyLeft => Ok(Self::KeyLeft),
Linux::KeyRight => Ok(Self::KeyRight),
Linux::KeyEnd => Ok(Self::KeyEnd),
Linux::KeyDown => Ok(Self::KeyDown),
Linux::KeyPagedown => Ok(Self::KeyPageDown),
Linux::KeyInsert => Ok(Self::KeyInsert),
Linux::KeyDelete => Ok(Self::KeyDeleteForward),
Linux::KeyMacro => Err(()), // TODO
Linux::KeyMute => Ok(Self::KeyMute),
Linux::KeyVolumeDown => Ok(Self::KeyVolumeDown),
Linux::KeyVolumeUp => Ok(Self::KeyVolumeUp),
Linux::KeyPower => Ok(Self::Shutdown),
Linux::KeyKpequal => Ok(Self::KeypadEquals),
Linux::KeyKpplusminus => Ok(Self::KeypadPlus),
Linux::KeyPause => Ok(Self::KeyPause),
Linux::KeyScale => Err(()), // TODO
Linux::KeyKpcomma => Ok(Self::KeypadComma),
Linux::KeyHangeul => Ok(Self::KeyInternational1), // TODO unsure
Linux::KeyHanja => Ok(Self::KeyInternational2), // TODO unsure
Linux::KeyYen => Ok(Self::KeyInternational3), // TODO unsure
Linux::KeyLeftmeta => Ok(Self::KeyLeftGUI),
Linux::KeyRightmeta => Ok(Self::KeyRightGUI),
Linux::KeyCompose => Ok(Self::KeyApplication),
Linux::KeyStop => Ok(Self::ACStop),
Linux::KeyAgain => Err(()),
Linux::KeyProps => Err(()),
Linux::KeyUndo => Err(()),
Linux::KeyFront => Err(()),
Linux::KeyCopy => Err(()),
Linux::KeyOpen => Err(()),
Linux::KeyPaste => Err(()),
Linux::KeyFind => Ok(Self::ACSearch),
Linux::KeyCut => Err(()),
Linux::KeyHelp => Ok(Self::KeyF1), // AL Integrated Help Center?
Linux::KeyMenu => Ok(Self::KeyApplication),
Linux::KeyCalc => Ok(Self::ALCalculator),
Linux::KeySetup => Err(()),
Linux::KeySleep => Ok(Self::SystemSleep),
Linux::KeyWakeup => Ok(Self::SystemWakeUp),
Linux::KeyFile => Ok(Self::ALLocalMachineBrowser),
Linux::KeySendfile => Err(()),
Linux::KeyDeletefile => Err(()),
Linux::KeyXfer => Err(()),
Linux::KeyProg1 => Err(()),
Linux::KeyProg2 => Err(()),
Linux::KeyWww => Ok(Self::ACSearch), // TODO unsure
Linux::KeyMsdos => Err(()),
Linux::KeyCoffee => Err(()),
Linux::KeyRotateDisplay => Err(()),
Linux::KeyCyclewindows => Err(()),
Linux::KeyMail => Ok(Self::ALEmailReader),
Linux::KeyBookmarks => Ok(Self::ACBookmarks),
Linux::KeyComputer => Ok(Self::ACHome),
Linux::KeyBack => Ok(Self::ACBack),
Linux::KeyForward => Ok(Self::ACForward),
Linux::KeyClosecd => Err(()),
Linux::KeyEjectcd => Err(()),
Linux::KeyEjectclosecd => Err(()),
Linux::KeyNextsong => Ok(Self::KeyScanNextTrack),
Linux::KeyPlaypause => Ok(Self::KeyPlayPause),
Linux::KeyPrevioussong => Ok(Self::KeyScanPreviousTrack),
Linux::KeyStopcd => Ok(Self::KeyStop),
Linux::KeyRecord => Err(()),
Linux::KeyRewind => Err(()),
Linux::KeyPhone => Err(()),
Linux::KeyIso => Err(()),
Linux::KeyConfig => Err(()),
Linux::KeyHomepage => Ok(Self::ACHome),
Linux::KeyRefresh => Ok(Self::ACRefresh),
Linux::KeyExit => Err(()),
Linux::KeyMove => Err(()),
Linux::KeyEdit => Err(()),
Linux::KeyScrollup => Err(()),
Linux::KeyScrolldown => Err(()),
Linux::KeyKpleftparen => Err(()),
Linux::KeyKprightparen => Err(()),
Linux::KeyNew => Err(()),
Linux::KeyRedo => Err(()),
Linux::KeyF13 => Ok(Self::KeyF13),
Linux::KeyF14 => Ok(Self::KeyF14),
Linux::KeyF15 => Ok(Self::KeyF15),
Linux::KeyF16 => Ok(Self::KeyF16),
Linux::KeyF17 => Ok(Self::KeyF17),
Linux::KeyF18 => Ok(Self::KeyF18),
Linux::KeyF19 => Ok(Self::KeyF19),
Linux::KeyF20 => Ok(Self::KeyF20),
Linux::KeyF21 => Ok(Self::KeyF21),
Linux::KeyF22 => Ok(Self::KeyF22),
Linux::KeyF23 => Ok(Self::KeyF23),
Linux::KeyF24 => Ok(Self::KeyF24),
Linux::KeyPlaycd => Err(()),
Linux::KeyPausecd => Err(()),
Linux::KeyProg3 => Err(()),
Linux::KeyProg4 => Err(()),
Linux::KeyAllApplications => Err(()),
Linux::KeySuspend => Err(()),
Linux::KeyClose => Err(()),
Linux::KeyPlay => Err(()),
Linux::KeyFastforward => Err(()),
Linux::KeyBassboost => Err(()),
Linux::KeyPrint => Err(()),
Linux::KeyHp => Err(()),
Linux::KeyCamera => Err(()),
Linux::KeySound => Err(()),
Linux::KeyQuestion => Err(()),
Linux::KeyEmail => Err(()),
Linux::KeyChat => Err(()),
Linux::KeySearch => Err(()),
Linux::KeyConnect => Err(()),
Linux::KeyFinance => Err(()),
Linux::KeySport => Err(()),
Linux::KeyShop => Err(()),
Linux::KeyAlterase => Err(()),
Linux::KeyCancel => Err(()),
Linux::KeyBrightnessdown => Err(()),
Linux::KeyBrightnessup => Err(()),
Linux::KeyMedia => Err(()),
Linux::KeySwitchvideomode => Err(()),
Linux::KeyKbdillumtoggle => Err(()),
Linux::KeyKbdillumdown => Err(()),
Linux::KeyKbdillumup => Err(()),
Linux::KeySend => Err(()),
Linux::KeyReply => Err(()),
Linux::KeyForwardmail => Err(()),
Linux::KeySave => Err(()),
Linux::KeyDocuments => Err(()),
Linux::KeyBattery => Err(()),
Linux::KeyBluetooth => Err(()),
Linux::KeyWlan => Err(()),
Linux::KeyUwb => Err(()),
Linux::KeyUnknown => Err(()),
Linux::KeyVideoNext => Err(()),
Linux::KeyVideoPrev => Err(()),
Linux::KeyBrightnessCycle => Err(()),
Linux::KeyBrightnessAuto => Err(()),
Linux::KeyDisplayOff => Err(()),
Linux::KeyWwan => Err(()),
Linux::KeyRfkill => Err(()),
Linux::KeyMicmute => Err(()),
Linux::KeyCount => Err(()),
Linux::Invalid => Err(()),
Linux::Invalid1 => Err(()),
Linux::Invalid2 => Err(()),
Linux::Invalid3 => Err(()),
Linux::Invalid4 => Err(()),
Linux::Invalid5 => Err(()),
}
}
}

860
src/server.rs Normal file
View File

@@ -0,0 +1,860 @@
use anyhow::anyhow;
use futures::stream::StreamExt;
use log;
use std::{
cell::{Cell, RefCell},
collections::HashSet,
error::Error,
io::Result,
net::IpAddr,
rc::Rc,
time::{Duration, Instant},
};
use tokio::{io::ReadHalf, net::UdpSocket, signal, sync::mpsc::Sender, task};
#[cfg(unix)]
use tokio::net::UnixStream;
#[cfg(windows)]
use tokio::net::TcpStream;
use std::{io::ErrorKind, net::SocketAddr};
use crate::{
client::{ClientEvent, ClientHandle, ClientManager, Position},
config::Config,
consumer::EventConsumer,
dns,
frontend::{self, FrontendEvent, FrontendListener, FrontendNotify},
producer::EventProducer,
};
use crate::{
consumer,
event::{Event, KeyboardEvent},
producer,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum State {
Sending,
Receiving,
AwaitingLeave,
}
pub enum ProducerEvent {
Release,
ClientEvent(ClientEvent),
}
pub enum ConsumerEvent {
ClientEvent(ClientEvent),
}
#[derive(Clone)]
struct ClientUpdate {
client: ClientHandle,
hostname: Option<String>,
port: u16,
pos: Position,
}
pub struct Server {}
impl Server {
pub async fn run(config: &Config) -> anyhow::Result<()> {
// create frontend communication adapter
let mut frontend = match FrontendListener::new().await {
Some(Err(e)) => return Err(e),
Some(Ok(f)) => f,
None => {
// none means some other instance is already running
log::info!("service already running, exiting");
return anyhow::Ok(());
}
};
let (mut consumer, mut producer) = tokio::join!(consumer::create(), producer::create());
// create dns resolver
let resolver = dns::DnsResolver::new().await?;
// bind the udp socket
let listen_addr = SocketAddr::new("0.0.0.0".parse().unwrap(), config.port);
let mut socket = UdpSocket::bind(listen_addr).await?;
let (frontend_tx, mut frontend_rx) = tokio::sync::mpsc::channel(1);
// create client manager
let client_manager_rc = Rc::new(RefCell::new(ClientManager::new()));
let state_rc = Rc::new(Cell::new(State::Receiving));
// channel to notify producer
let (producer_notify_tx, mut producer_notify_rx) = tokio::sync::mpsc::channel(32);
// channel to notify consumer
let (consumer_notify_tx, mut consumer_notify_rx) = tokio::sync::mpsc::channel(32);
// channel to request dns resolver
let (resolve_tx, mut resolve_rx) = tokio::sync::mpsc::channel(32);
// channel to send events to frontends
let (frontend_notify_tx, mut frontend_notify_rx) = tokio::sync::mpsc::channel(32);
// channels for udp send / receive
let (receiver_tx, mut receiver_rx) = tokio::sync::mpsc::channel(32);
let (sender_tx, mut sender_rx) = tokio::sync::mpsc::channel(32);
let (port_tx, mut port_rx) = tokio::sync::mpsc::channel(32);
// add clients from config
for (c, h, port, p) in config.get_clients().into_iter() {
Self::add_client(
&resolve_tx,
&client_manager_rc,
&mut frontend,
h,
c,
port,
p,
)
.await;
}
// event producer
let client_manager = client_manager_rc.clone();
let state = state_rc.clone();
let sender_ch = sender_tx.clone();
let producer_task = tokio::task::spawn_local(async move {
loop {
tokio::select! {
e = producer.next() => {
let (client, event) = match e {
Some(e) => e?,
None => return Err::<(), anyhow::Error>(anyhow!("event producer closed")),
};
Self::handle_producer_event(&mut producer, &client_manager, &state, &sender_ch, client, event).await;
}
e = producer_notify_rx.recv() => {
match e {
Some(e) => match e {
ProducerEvent::Release => producer.release(),
ProducerEvent::ClientEvent(e) => producer.notify(e),
},
None => break Ok(()),
}
}
}
}
});
// event consumer
let client_manager = client_manager_rc.clone();
let state = state_rc.clone();
let producer_notify = producer_notify_tx.clone();
let receiver_task = tokio::task::spawn_local(async move {
let mut last_ignored = None;
loop {
tokio::select! {
udp_event = receiver_rx.recv() => {
let udp_event = match udp_event {
Some(Ok(e)) => e,
Some(Err(e)) => return Err::<(), anyhow::Error>(anyhow!("{}", e)),
None => return Err::<(), anyhow::Error>(anyhow!("receiver closed")),
};
Self::handle_udp_rx(&client_manager, &producer_notify, &mut consumer, &sender_tx, &state, &mut last_ignored, udp_event).await;
}
consumer_event = consumer_notify_rx.recv() => {
match consumer_event {
Some(e) => match e {
ConsumerEvent::ClientEvent(e) => consumer.notify(e).await,
},
None => break,
}
}
_ = consumer.dispatch() => { }
}
}
// destroy consumer
consumer.destroy().await;
Ok(())
});
// frontend listener
let client_manager = client_manager_rc.clone();
let frontend_task = tokio::task::spawn_local(async move {
loop {
tokio::select! {
stream = frontend.accept() => {
let stream = match stream {
Ok(s) => s,
Err(e) => {
log::warn!("error accepting frontend connection: {e}");
continue;
}
};
Self::handle_frontend_stream(&client_manager, &mut frontend, &frontend_tx, stream).await;
}
event = frontend_rx.recv() => {
let frontend_event = match event {
Some(e) => e,
None => return Err::<(), anyhow::Error>(anyhow!("frontend channel closed")),
};
let exit = Self::handle_frontend_event(&producer_notify_tx, &consumer_notify_tx, &client_manager, &resolve_tx, &mut frontend, &port_tx, frontend_event).await;
if exit {
return Ok(());
}
}
notify = frontend_notify_rx.recv() => {
let notify = match notify {
Some(n) => n,
None => return Err::<(), anyhow::Error>(anyhow!("frontend notify closed")),
};
let _ = frontend.notify_all(notify).await;
}
}
}
});
// dns resolver
let client_manager = client_manager_rc.clone();
let resolver_task = tokio::task::spawn_local(async move {
loop {
let (host, client): (String, ClientHandle) = match resolve_rx.recv().await {
Some(r) => r,
None => break,
};
let ips = match resolver.resolve(&host).await {
Ok(ips) => ips,
Err(e) => {
log::warn!("could not resolve host '{host}': {e}");
continue;
}
};
if let Some(state) = client_manager.borrow_mut().get_mut(client) {
let port = state.client.port;
let mut addrs = HashSet::from_iter(
state
.client
.fix_ips
.iter()
.map(|a| SocketAddr::new(*a, port)),
);
for ip in ips {
let sock_addr = SocketAddr::new(ip, port);
addrs.insert(sock_addr);
}
state.client.addrs = addrs;
}
}
});
// udp task
let udp_task = tokio::task::spawn_local(async move {
loop {
tokio::select! {
event = receive_event(&socket) => {
let _ = receiver_tx.send(event).await;
}
event = sender_rx.recv() => {
let Some((event, addr)) = event else {
break;
};
if let Err(e) = send_event(&socket, event, addr) {
log::warn!("udp send failed: {e}");
};
}
port = port_rx.recv() => {
let Some(port) = port else {
break;
};
let current_port = socket.local_addr().unwrap().port();
if current_port == port {
let _ = frontend_notify_tx.send(FrontendNotify::NotifyPortChange(port, None)).await;
continue;
};
let listen_addr = SocketAddr::new("0.0.0.0".parse().unwrap(), port);
match UdpSocket::bind(listen_addr).await {
Ok(new_socket) => {
socket = new_socket;
let _ = frontend_notify_tx.send(FrontendNotify::NotifyPortChange(port, None)).await;
}
Err(e) => {
log::warn!("could not change port: {e}");
let port = socket.local_addr().unwrap().port();
let _ = frontend_notify_tx.send(FrontendNotify::NotifyPortChange(
port,
Some(format!("could not change port: {e}")),
)).await;
}
}
}
}
}
});
let reaper = task::spawn_local(async move {
tokio::select! {
_ = signal::ctrl_c() => {
log::info!("terminating service");
},
_ = producer_task => {
// TODO restart producer?
}
_ = receiver_task => {
// TODO restart producer?
}
_ = frontend_task => {
// frontend exited => exit requested
}
_ = resolver_task => {
// resolver exited
}
_ = udp_task => {
// udp exited
}
}
});
reaper.await?;
Ok(())
}
pub async fn add_client(
resolver_tx: &Sender<(String, ClientHandle)>,
client_manager: &Rc<RefCell<ClientManager>>,
frontend: &mut FrontendListener,
hostname: Option<String>,
addr: HashSet<IpAddr>,
port: u16,
pos: Position,
) -> ClientHandle {
log::info!(
"adding client [{}]{} @ {:?}",
pos,
hostname.as_deref().unwrap_or(""),
&addr
);
let client = client_manager
.borrow_mut()
.add_client(hostname.clone(), addr, port, pos);
log::debug!("add_client {client}");
if let Some(hostname) = hostname.clone() {
let _ = resolver_tx.send((hostname, client)).await;
};
let notify = FrontendNotify::NotifyClientCreate(client, hostname, port, pos);
if let Err(e) = frontend.notify_all(notify).await {
log::error!("error notifying frontend: {e}");
};
client
}
pub async fn activate_client(
producer_notify_tx: &Sender<ProducerEvent>,
consumer_notify_tx: &Sender<ConsumerEvent>,
client_manager: &Rc<RefCell<ClientManager>>,
client: ClientHandle,
active: bool,
) {
let (client, pos) = match client_manager.borrow_mut().get_mut(client) {
Some(state) => {
state.active = active;
(state.client.handle, state.client.pos)
}
None => return,
};
if active {
let _ = producer_notify_tx
.send(ProducerEvent::ClientEvent(ClientEvent::Create(client, pos)))
.await;
let _ = consumer_notify_tx
.send(ConsumerEvent::ClientEvent(ClientEvent::Create(client, pos)))
.await;
} else {
let _ = producer_notify_tx
.send(ProducerEvent::ClientEvent(ClientEvent::Destroy(client)))
.await;
let _ = consumer_notify_tx
.send(ConsumerEvent::ClientEvent(ClientEvent::Destroy(client)))
.await;
}
}
pub async fn remove_client(
client_manager: &Rc<RefCell<ClientManager>>,
producer_notify_tx: &Sender<ProducerEvent>,
consumer_notify_tx: &Sender<ConsumerEvent>,
frontend: &mut FrontendListener,
client: ClientHandle,
) -> Option<ClientHandle> {
let _ = producer_notify_tx
.send(ProducerEvent::ClientEvent(ClientEvent::Destroy(client)))
.await;
let _ = consumer_notify_tx
.send(ConsumerEvent::ClientEvent(ClientEvent::Destroy(client)))
.await;
let Some(client) = client_manager
.borrow_mut()
.remove_client(client)
.map(|s| s.client.handle)
else {
return None;
};
let notify = FrontendNotify::NotifyClientDelete(client);
log::debug!("{notify:?}");
if let Err(e) = frontend.notify_all(notify).await {
log::error!("error notifying frontend: {e}");
}
Some(client)
}
async fn update_client(
producer_notify_tx: &Sender<ProducerEvent>,
consumer_notify_tx: &Sender<ConsumerEvent>,
resolve_tx: &Sender<(String, ClientHandle)>,
client_manager: &Rc<RefCell<ClientManager>>,
client_update: ClientUpdate,
) {
let (hostname, handle, active) = {
// retrieve state
let mut client_manager = client_manager.borrow_mut();
let Some(state) = client_manager.get_mut(client_update.client) else {
return;
};
// update pos
state.client.pos = client_update.pos;
// update port
if state.client.port != client_update.port {
state.client.port = client_update.port;
state.client.addrs = state
.client
.addrs
.iter()
.cloned()
.map(|mut a| {
a.set_port(client_update.port);
a
})
.collect();
state
.client
.active_addr
.map(|a| SocketAddr::new(a.ip(), client_update.port));
}
// update hostname
if state.client.hostname != client_update.hostname {
state.client.addrs = HashSet::new();
state.client.active_addr = None;
state.client.hostname = client_update.hostname;
}
log::debug!("client updated: {:?}", state);
(
state.client.hostname.clone(),
state.client.handle,
state.active,
)
};
// resolve dns
if let Some(hostname) = hostname {
let _ = resolve_tx.send((hostname, handle)).await;
}
// update state in event consumer & producer
if active {
let _ = producer_notify_tx
.send(ProducerEvent::ClientEvent(ClientEvent::Destroy(
client_update.client,
)))
.await;
let _ = consumer_notify_tx
.send(ConsumerEvent::ClientEvent(ClientEvent::Destroy(
client_update.client,
)))
.await;
let _ = producer_notify_tx
.send(ProducerEvent::ClientEvent(ClientEvent::Create(
client_update.client,
client_update.pos,
)))
.await;
let _ = consumer_notify_tx
.send(ConsumerEvent::ClientEvent(ClientEvent::Create(
client_update.client,
client_update.pos,
)))
.await;
}
}
async fn handle_udp_rx(
client_manager: &Rc<RefCell<ClientManager>>,
producer_notify_tx: &Sender<ProducerEvent>,
consumer: &mut Box<dyn EventConsumer>,
sender_tx: &Sender<(Event, SocketAddr)>,
state: &Rc<Cell<State>>,
last_ignored: &mut Option<SocketAddr>,
event: (Event, SocketAddr),
) {
let (event, addr) = event;
// get handle for addr
let handle = match client_manager.borrow().get_client(addr) {
Some(a) => a,
None => {
if last_ignored.is_none() || last_ignored.is_some() && last_ignored.unwrap() != addr
{
log::warn!("ignoring events from client {addr}");
last_ignored.replace(addr);
}
return;
}
};
// next event can be logged as ignored again
last_ignored.take();
log::trace!("{:20} <-<-<-<------ {addr} ({handle})", event.to_string());
{
let mut client_manager = client_manager.borrow_mut();
let client_state = match client_manager.get_mut(handle) {
Some(s) => s,
None => {
log::error!("unknown handle");
return;
}
};
// reset ttl for client and
client_state.last_seen = Some(Instant::now());
// set addr as new default for this client
client_state.client.active_addr = Some(addr);
}
match (event, addr) {
(Event::Pong(), _) => { /* ignore pong events */ }
(Event::Ping(), addr) => {
let _ = sender_tx.send((Event::Pong(), addr)).await;
}
(event, addr) => {
// tell clients that we are ready to receive events
if let Event::Enter() = event {
let _ = sender_tx.send((Event::Leave(), addr)).await;
}
match state.get() {
State::Sending => {
if let Event::Leave() = event {
// ignore additional leave events that may
// have been sent for redundancy
} else {
// upon receiving any event, we go back to receiving mode
let _ = producer_notify_tx.send(ProducerEvent::Release).await;
state.replace(State::Receiving);
log::trace!("STATE ===> Receiving");
}
}
State::Receiving => {
// consume event
consumer.consume(event, handle).await;
log::trace!("{event:?} => consumer");
}
State::AwaitingLeave => {
// we just entered the deadzone of a client, so
// we need to ignore events that may still
// be on the way until a leave event occurs
// telling us the client registered the enter
if let Event::Leave() = event {
state.replace(State::Sending);
log::trace!("STATE ===> Sending");
}
// entering a client that is waiting for a leave
// event should still be possible
if let Event::Enter() = event {
state.replace(State::Receiving);
log::trace!("STATE ===> Receiving");
let _ = producer_notify_tx.send(ProducerEvent::Release).await;
}
}
}
}
}
let pong = {
let mut client_manager = client_manager.borrow_mut();
let client_state = match client_manager.get_mut(handle) {
Some(s) => s,
None => {
log::error!("unknown handle");
return;
}
};
// let the server know we are still alive once every second
if client_state.last_replied.is_none()
|| client_state.last_replied.is_some()
&& client_state.last_replied.unwrap().elapsed() > Duration::from_secs(1)
{
client_state.last_replied = Some(Instant::now());
true
} else {
false
}
};
if pong {
let _ = sender_tx.send((Event::Pong(), addr)).await;
}
}
const RELEASE_MODIFIERDS: u32 = 77; // ctrl+shift+super+alt
async fn handle_producer_event(
producer: &mut Box<dyn EventProducer>,
client_manager: &Rc<RefCell<ClientManager>>,
state: &Rc<Cell<State>>,
sender_tx: &Sender<(Event, SocketAddr)>,
c: ClientHandle,
mut e: Event,
) {
log::trace!("producer: ({c}) {e:?}");
if let Event::Keyboard(crate::event::KeyboardEvent::Modifiers {
mods_depressed,
mods_latched: _,
mods_locked: _,
group: _,
}) = e
{
if mods_depressed == Self::RELEASE_MODIFIERDS {
producer.release();
state.replace(State::Receiving);
log::trace!("STATE ===> Receiving");
// send an event to release all the modifiers
e = Event::Keyboard(KeyboardEvent::Modifiers {
mods_depressed: 0,
mods_latched: 0,
mods_locked: 0,
group: 0,
});
}
}
let (addr, enter, ping_addrs) = {
let mut enter = false;
let mut ping_addrs: Option<Vec<SocketAddr>> = None;
// get client state for handle
let mut client_manager = client_manager.borrow_mut();
let client_state = match client_manager.get_mut(c) {
Some(state) => state,
None => {
// should not happen
log::warn!("unknown client!");
producer.release();
state.replace(State::Receiving);
log::trace!("STATE ===> Receiving");
return;
}
};
// if we just entered the client we want to send additional enter events until
// we get a leave event
if let State::Receiving | State::AwaitingLeave = state.get() {
state.replace(State::AwaitingLeave);
log::trace!("STATE ===> AwaitingLeave");
enter = true;
}
let last_seen = match client_state.last_seen {
None => Duration::MAX,
Some(i) => i.elapsed(),
};
let last_pinged = match client_state.last_ping {
None => Duration::MAX,
Some(i) => i.elapsed(),
};
// not seen for one second but pinged at least 500ms ago
if last_seen > Duration::from_secs(1)
&& last_pinged > Duration::from_millis(500)
&& last_pinged < Duration::from_secs(1)
{
// client unresponsive -> set state to receiving
if state.get() != State::Receiving {
log::info!("client not responding - releasing pointer");
producer.release();
state.replace(State::Receiving);
log::trace!("STATE ===> Receiving");
}
}
// last ping > 500ms ago -> ping all interfaces
if last_pinged > Duration::from_millis(500) {
ping_addrs = Some(client_state.client.addrs.iter().cloned().collect());
client_state.last_ping = Some(Instant::now());
}
(client_state.client.active_addr, enter, ping_addrs)
};
if let Some(addr) = addr {
if enter {
let _ = sender_tx.send((Event::Enter(), addr)).await;
}
let _ = sender_tx.send((e, addr)).await;
}
if let Some(addrs) = ping_addrs {
for addr in addrs {
let _ = sender_tx.send((Event::Ping(), addr)).await;
}
}
}
async fn handle_frontend_stream(
client_manager: &Rc<RefCell<ClientManager>>,
frontend: &mut FrontendListener,
frontend_tx: &Sender<FrontendEvent>,
#[cfg(unix)] mut stream: ReadHalf<UnixStream>,
#[cfg(windows)] mut stream: ReadHalf<TcpStream>,
) {
use std::io;
let tx = frontend_tx.clone();
tokio::task::spawn_local(async move {
loop {
let event = frontend::read_event(&mut stream).await;
match event {
Ok(event) => {
let _ = tx.send(event).await;
}
Err(e) => {
if let Some(e) = e.downcast_ref::<io::Error>() {
if e.kind() == ErrorKind::UnexpectedEof {
return;
}
}
log::error!("error reading frontend event: {e}");
return;
}
}
}
});
Self::enumerate(client_manager, frontend).await;
}
async fn handle_frontend_event(
producer_notify_tx: &Sender<ProducerEvent>,
consumer_notify_tx: &Sender<ConsumerEvent>,
client_manager: &Rc<RefCell<ClientManager>>,
resolve_tx: &Sender<(String, ClientHandle)>,
frontend: &mut FrontendListener,
port_tx: &Sender<u16>,
event: FrontendEvent,
) -> bool {
log::debug!("frontend: {event:?}");
match event {
FrontendEvent::AddClient(hostname, port, pos) => {
Self::add_client(
resolve_tx,
client_manager,
frontend,
hostname,
HashSet::new(),
port,
pos,
)
.await;
}
FrontendEvent::ActivateClient(client, active) => {
Self::activate_client(
producer_notify_tx,
consumer_notify_tx,
client_manager,
client,
active,
)
.await
}
FrontendEvent::ChangePort(port) => {
let _ = port_tx.send(port).await;
}
FrontendEvent::DelClient(client) => {
Self::remove_client(
client_manager,
producer_notify_tx,
consumer_notify_tx,
frontend,
client,
)
.await;
}
FrontendEvent::Enumerate() => Self::enumerate(client_manager, frontend).await,
FrontendEvent::Shutdown() => {
log::info!("terminating gracefully...");
return true;
}
FrontendEvent::UpdateClient(client, hostname, port, pos) => {
let client_update = ClientUpdate {
client,
hostname,
port,
pos,
};
Self::update_client(
producer_notify_tx,
consumer_notify_tx,
resolve_tx,
client_manager,
client_update,
)
.await
}
}
false
}
async fn enumerate(
client_manager: &Rc<RefCell<ClientManager>>,
frontend: &mut FrontendListener,
) {
let clients = client_manager
.borrow()
.get_client_states()
.map(|s| (s.client.clone(), s.active))
.collect();
if let Err(e) = frontend
.notify_all(FrontendNotify::Enumerate(clients))
.await
{
log::error!("error notifying frontend: {e}");
}
}
}
async fn receive_event(
socket: &UdpSocket,
) -> std::result::Result<(Event, SocketAddr), Box<dyn Error>> {
let mut buf = vec![0u8; 22];
match socket.recv_from(&mut buf).await {
Ok((_amt, src)) => Ok((Event::try_from(buf)?, src)),
Err(e) => Err(Box::new(e)),
}
}
fn send_event(sock: &UdpSocket, e: Event, addr: SocketAddr) -> Result<usize> {
log::trace!("{:20} ------>->->-> {addr}", e.to_string());
let data: Vec<u8> = (&e).into();
// When udp blocks, we dont want to block the event loop.
// Dropping events is better than potentially crashing the event
// producer.
sock.try_send_to(&data, addr)
}