Compare commits

..

149 Commits

Author SHA1 Message Date
Ferdinand Schober
db96717044 chore: Release lan-mouse version 0.7.3 2024-03-22 12:46:57 +01:00
Ferdinand Schober
be8124a190 fix dns resolving 2024-03-22 12:40:05 +01:00
Ferdinand Schober
dcee2933a2 Create FUNDING.yml 2024-03-22 10:09:00 +01:00
Ferdinand Schober
8aaff9fb58 move to windows from win-api (#99) 2024-03-21 23:04:20 +01:00
Ferdinand Schober
742b1585d7 rename producer, consumer to emulation and capture (#98)
input emulation / input capture is clearer than event consumer and producer
2024-03-21 20:26:57 +01:00
Ferdinand Schober
78c9de45c7 add an arm64 build (#45)
closes #45
2024-03-21 17:14:33 +01:00
Ferdinand Schober
a491c0e9e3 refactor producer and consumer (#97) 2024-03-21 16:55:54 +01:00
Ferdinand Schober
af02cccc2a exit instead of panicing when con to backend lost 2024-03-21 15:35:56 +01:00
Ferdinand Schober
4a6399f866 Update README.md - now available on crates.io 2024-03-21 13:37:01 +01:00
Ferdinand Schober
66bce9083e chore: Release lan-mouse version 0.7.2 2024-03-21 13:06:28 +01:00
Ferdinand Schober
102b64f2b4 chore: Release lan-mouse version 0.7.1 2024-03-21 13:04:22 +01:00
Ferdinand Schober
4b499742ad update dependencies 2024-03-21 13:02:21 +01:00
Ferdinand Schober
a86d74b52c update to reis 0.2 2024-03-21 12:50:47 +01:00
Ferdinand Schober
c25a15e2d8 CI: fix download-artifact job 2024-03-20 15:28:05 +01:00
Ferdinand Schober
8b82325bdb update flake.lock, Cargo.lock 2024-03-20 14:48:29 +01:00
Ferdinand Schober
5415205c83 chore: Release lan-mouse version 0.7.0 2024-03-20 14:31:25 +01:00
Ferdinand Schober
1666fb8b7b Update README.md 2024-03-20 14:18:59 +01:00
Ferdinand Schober
9afe7da0dd Libei Input Capture (#94) 2024-03-20 14:03:52 +01:00
Ferdinand Schober
f7c59e40c9 Update config.toml 2024-03-19 13:14:53 +01:00
Ferdinand Schober
5be5b0ad7c Update README.md 2024-03-19 13:14:36 +01:00
Ferdinand Schober
8ed4520172 Update README.md (#96)
* Update README.md

include release_bind in example config

* update config.toml as well
2024-03-18 09:54:38 +01:00
Ferdinand Schober
9e56c546cd make release bind configurable (#95)
closes #85
2024-03-18 09:20:28 +01:00
Ferdinand Schober
daf8818a9f Update README.md
closes #80
2024-03-17 11:23:32 +01:00
Ferdinand Schober
6eaa199503 update actions (#93) 2024-03-15 22:09:09 +01:00
虢豳
8ff991aefe feat: add nix support(#80) (#82)
* feat: add nix support(#80)

* chore: nix flake update

* update actions
2024-03-15 22:02:18 +01:00
Ferdinand Schober
abf95afb9f Update README.md
Remove Hyprland section - all issues are fixed
2024-03-15 17:27:24 +01:00
Ferdinand Schober
9a75a7622e fix duplicate creating of wl_pointer / wl_keyboard (#92)
closes #91
2024-03-15 17:00:59 +01:00
Ferdinand Schober
0196cfe56c Update README.md
sway 1.9 works
2024-03-03 12:02:38 +01:00
Ferdinand Schober
097468f708 Update README.md
closes #90
2024-03-02 11:58:33 +01:00
Ferdinand Schober
3470abc03a Update README.md
update issues in Hyprland
2024-03-01 13:35:12 +01:00
Ferdinand Schober
a7397ad4f4 Update README.md
fix typo
2024-02-05 16:08:25 +00:00
Ferdinand Schober
9889b49f10 Update README.md 2024-01-30 20:22:51 +01:00
Ferdinand Schober
f4db2366b7 chore: Release lan-mouse version 0.6.0 2024-01-28 17:35:52 +01:00
Ferdinand Schober
c9deb6eba4 add firewalld config file 2024-01-28 17:32:42 +01:00
Kai
5cc8cda19d macos: add keyboard support (#81)
* macos: add keyboard support

* macos: handle key repeat

* update README
2024-01-26 11:05:54 +01:00
Ferdinand Schober
8084b52cfc Revert "gtk: handle exit of service properly"
This reverts commit 1f4821a16d.
breaks ubuntu lts
2024-01-23 21:51:40 +01:00
Ferdinand Schober
1f4821a16d gtk: handle exit of service properly 2024-01-23 21:36:42 +01:00
Ferdinand Schober
82926d8272 fix error handling in consumer task 2024-01-23 20:47:29 +01:00
Ferdinand Schober
006831b9f1 add systemd user service definition
ref #76
ref #49
2024-01-21 20:44:09 +01:00
Ferdinand Schober
e5b770a799 Update README.md
Add installation instructions
2024-01-19 14:14:47 +01:00
Ferdinand Schober
017bc43176 refactor timer task 2024-01-19 02:07:03 +01:00
Ferdinand Schober
36001c6fb2 refactor udp task 2024-01-19 02:03:30 +01:00
Ferdinand Schober
2803db7073 refactor dns task 2024-01-19 02:01:45 +01:00
Ferdinand Schober
622b04b36c refactor frontend task 2024-01-19 01:58:49 +01:00
Ferdinand Schober
61ff05c95a refactor consumer task 2024-01-19 01:51:09 +01:00
Ferdinand Schober
ecab3a360d refactor producer task 2024-01-18 23:46:06 +01:00
Ferdinand Schober
6674af8e63 allow incoming requests from arbitrary ports (#78)
closes #77
2024-01-18 22:36:33 +01:00
Ferdinand Schober
b3caba99ab fix misleading warning 2024-01-18 22:22:27 +01:00
Ferdinand Schober
fad48c2504 no config is not an error 2024-01-17 08:37:18 +01:00
Ferdinand Schober
f28f75418c add a warning when mouse is released by compositor
This can not be influenced and is helpful for debugging
2024-01-17 08:36:41 +01:00
Ferdinand Schober
e2c47d3096 fix: initial dns resolve was not working 2024-01-17 00:22:24 +01:00
Ferdinand Schober
f19944515a Revert "temporary fix for AUR pkg"
This reverts commit 8c276f88b7.
2024-01-16 23:11:13 +01:00
Ferdinand Schober
535cd055b9 fix initial activation 2024-01-16 19:49:34 +01:00
Ferdinand Schober
118c0dfc73 cleanup 2024-01-16 16:58:47 +01:00
Ferdinand Schober
7897db6047 remove unneccessary enumerate request 2024-01-16 16:15:23 +01:00
Ferdinand Schober
347256e966 fix frontend channel buffer size 2024-01-16 16:03:33 +01:00
Ferdinand Schober
0017dbc634 Update README.md 2024-01-16 13:01:25 +01:00
Ferdinand Schober
d90eb0cd0f Activate on startup (#70)
Frontends are now properly synced among each other and on startup the correct state is reflected.

Closes #75 
Closes #68
2024-01-16 12:59:39 +01:00
Ferdinand Schober
2e52660714 fix name of desktop entry 2024-01-15 11:06:00 +01:00
Ferdinand Schober
8c276f88b7 temporary fix for AUR pkg 2024-01-15 09:00:21 +01:00
Ferdinand Schober
13597b3587 fix app_id + app icon 2024-01-15 08:42:23 +01:00
CupricReki
b59808742a Modified .desktop file to conform with standard (#72) 2024-01-15 07:28:38 +01:00
Ferdinand Schober
6c99f9bea3 chore: Release lan-mouse version 0.5.1 2024-01-12 13:46:56 +01:00
Ferdinand Schober
d54b3a08e1 ignore double press / release events (#71)
This should fix most of the remaining issues with stuck keys in KDE
2024-01-12 13:13:23 +01:00
Ferdinand Schober
767fc8bd6b comment about pointer relase in sending state
Figured out, why it sometimes happened that the pointer is released in
sending state. However nothing we can do about it.

(not a real issue)
2024-01-08 16:54:14 +01:00
Ferdinand Schober
fa15048ad8 ignore every event except Enter in receiving mode (#65)
Modifier or other key events are still sent by some compositors after leaving the window which causes them to be pressed on both devices.
Now an Enter event must be produced before any further events are sent out (except disconnect)
2024-01-05 18:00:30 +01:00
Ferdinand Schober
eb366bcd34 Update README.md
fix typos
2024-01-05 17:23:36 +01:00
Ferdinand Schober
91176b1267 Update README.md
see #64
2024-01-05 17:14:03 +01:00
Ferdinand Schober
a6f386ea83 windows: impl key repeating (#63)
closes #47
2024-01-05 13:19:41 +01:00
Ferdinand Schober
40b0cdd52e Add security disclaimer 2024-01-03 16:52:56 +01:00
Ferdinand Schober
1553ed4212 Update README.md 2024-01-02 22:46:27 +01:00
Ferdinand Schober
4561c20610 layer-shell: recreate windows, when output is removed / added (#62)
Previously, when an output was removed (e.g. laptop lid closed and opened) the windows used for input capture would not appear anymore until toggling the client off and on
2024-01-01 22:48:44 +01:00
Ferdinand Schober
6cdb607b11 Fix Error handling in layershell producer (#61)
previous error handling resulted in a softlock when the connection
to the compositor was lost
2024-01-01 22:07:21 +01:00
Ferdinand Schober
f5827bb31c fix port changing 2024-01-01 14:55:29 +01:00
Ferdinand Schober
64e3bf3ff4 release stuck keys (#53)
Keys are now released when
- A client disconnects and still has pressed keys
- A client disconnects through CTLR+ALT+SHIFT+WIN
- Lan Mouse terminates with keys still being pressed through a remote client

This is also fixes an issue caused by KDE's implementation of the remote desktop portal backend:
Keys are not correctly released when a remote desktop session is closed while keys are still pressed,
causing them to be permanently stuck until kwin is restarted.

This workaround remembers all pressed keys and releases them when lan-mouse exits or a device disconnects.

closes #15
2023-12-31 15:42:29 +01:00
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
79 changed files with 8283 additions and 2748 deletions

1
.envrc Normal file
View File

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

1
.github/FUNDING.yml vendored Normal file
View File

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

24
.github/workflows/cachix.yml vendored Normal file
View File

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

View File

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

View File

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

View File

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

6
.gitignore vendored
View File

@@ -1 +1,7 @@
/target
.gdbinit
.idea/
.vs/
.vscode/
.direnv/
result

1622
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.3.1"
version = "0.7.3"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/ferdinandschober/lan-mouse"
@@ -16,47 +16,48 @@ lto = "fat"
tempfile = "3.8"
trust-dns-resolver = "0.23"
memmap = "0.7"
toml = "0.7"
toml = "0.8"
serde = { version = "1.0", features = ["derive"] }
anyhow = "1.0.71"
log = "0.4.20"
env_logger = "0.10.0"
mio = { version = "0.8", features = ["os-ext"] }
libc = "0.2.148"
env_logger = "0.11.3"
serde_json = "1.0.107"
tokio = {version = "1.32.0", features = ["io-util", "macros", "net", "rt", "sync", "signal"] }
async-trait = "0.1.73"
futures-core = "0.3.28"
futures = "0.3.28"
clap = { version="4.4.11", features = ["derive"] }
gtk = { package = "gtk4", version = "0.8.1", features = ["v4_2"], optional = true }
adw = { package = "libadwaita", version = "0.6.0", features = ["v1_1"], optional = true }
async-channel = { version = "2.1.1", optional = true }
keycode = "0.4.0"
once_cell = "1.19.0"
[target.'cfg(unix)'.dependencies]
wayland-client = { version="0.30.2", optional = true }
wayland-protocols = { version="0.30.0", features=["client", "staging", "unstable"], optional = true }
wayland-protocols-wlr = { version="0.1.0", features=["client"], optional = true }
wayland-protocols-misc = { version="0.1.0", features=["client"], optional = true }
wayland-protocols-plasma = { version="0.1.0", features=["client"], optional = true }
mio-signals = "0.2.0"
libc = "0.2.148"
[target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
wayland-client = { version="0.31.1", optional = true }
wayland-protocols = { version="0.31.0", features=["client", "staging", "unstable"], optional = true }
wayland-protocols-wlr = { version="0.2.0", features=["client"], optional = true }
wayland-protocols-misc = { version="0.2.0", features=["client"], optional = true }
x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
gtk = { package = "gtk4", version = "0.7.2", features = ["v4_6"], optional = true }
adw = { package = "libadwaita", version = "0.5.2", features = ["v1_1"], optional = true }
ashpd = { version = "0.8", default-features = false, features = ["tokio"], optional = true }
reis = { version = "0.2", 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"] }
windows = { version = "0.54.0", features = [ "Win32_UI_Input_KeyboardAndMouse" ] }
[target.'cfg(unix)'.build-dependencies]
glib-build-tools = "0.18.0"
[build-dependencies]
glib-build-tools = "0.19.0"
[features]
default = [
"wayland",
"x11",
"xdg_desktop_portal",
"libei",
"gtk",
]
wayland = [
"dep:wayland-client",
"dep:wayland-protocols",
"dep:wayland-protocols-wlr",
"dep:wayland-protocols-misc",
"dep:wayland-protocols-plasma" ]
x11 = [ "dep:x11" ]
xdg_desktop_portal = []
libei = []
gtk = ["dep:gtk", "dep:adw"]
default = ["wayland", "x11", "xdg_desktop_portal", "libei", "gtk"]
wayland = ["dep:wayland-client", "dep:wayland-protocols", "dep:wayland-protocols-wlr", "dep:wayland-protocols-misc" ]
x11 = ["dep:x11"]
xdg_desktop_portal = ["dep:ashpd"]
libei = ["dep:reis", "dep:ashpd"]
gtk = ["dep:gtk", "dep:adw", "dep:async-channel"]

447
README.md
View File

@@ -1,60 +1,90 @@
# 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.
![image](https://github.com/ferdinandschober/lan-mouse/assets/40996949/ccb33815-4357-4c8d-a5d2-8897ab626a08)
The primary target is Wayland on Linux but Windows and MacOS and Linux on Xorg have partial support as well (see below for more details).
- _Now with a gtk frontend_
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/feschber/lan-mouse/assets/40996949/016a06a9-76db-4951-9dcc-127d012c59df">
<source media="(prefers-color-scheme: light)" srcset="https://github.com/feschber/lan-mouse/assets/40996949/d6318340-f811-4e16-9d6e-d1b79883c709">
<img alt="Screenshot of Lan-Mouse" srcset="https://github.com/feschber/lan-mouse/assets/40996949/016a06a9-76db-4951-9dcc-127d012c59df">
</picture>
Goal of this project is to be an open-source replacement for proprietary tools like [Synergy](https://symless.com/synergy), [Share Mouse](https://www.sharemouse.com/de/).
Goal of this project is to be an open-source replacement for proprietary tools like [Synergy 2/3](https://symless.com/synergy), [Share Mouse](https://www.sharemouse.com/de/).
Focus lies on performance and a clean, manageable implementation that can easily be expanded to support additional backends like e.g. Android, iOS, ... .
Of course ***blazingly fast™*** and stable, because it's written in rust.
***blazingly fast™*** because it's written in rust.
For an alternative (with slightly different goals) you may check out [Input Leap](https://github.com/input-leap).
_Now with a gtk frontend_
## Configuration
Configuration is done through the file `config.toml`,
which must be located in the current working directory when
executing lan-mouse.
> [!WARNING]
> Since this tool has gained a bit of popularity over the past couple of days:
>
> All network traffic is currently **unencrypted** and sent in **plaintext**.
>
> A malicious actor with access to the network could read input data or send input events with spoofed IPs to take control over a device.
>
> Therefore you should only use this tool in your local network with trusted devices for now
> and I take no responsibility for any leakage of data!
### Example config
A minimal config file could look like this:
```toml
# example configuration
## OS Support
# optional port (defaults to 4242)
port = 4242
# # optional frontend -> defaults to gtk if available
# # possible values are "cli" and "gtk"
# frontend = "gtk"
The following table shows support for input emulation (to emulate events received from other clients) and
input capture (to send events *to* other clients) on different operating systems:
# define a client on the right side with host name "iridium"
[right]
# hostname
host_name = "iridium"
# optional list of (known) ip addresses
ips = ["192.168.178.156"]
| Backend | input emulation | input capture |
|---------------------------|--------------------------|--------------------------------------|
| Wayland (wlroots) | :heavy_check_mark: | :heavy_check_mark: |
| Wayland (KDE) | :heavy_check_mark: | :heavy_check_mark: |
| Wayland (Gnome) | :heavy_check_mark: | :heavy_check_mark: |
| X11 | :heavy_check_mark: | WIP |
| Windows | :heavy_check_mark: | WIP |
| MacOS | :heavy_check_mark: | WIP |
# define a client on the left side with IP address 192.168.178.189
[left]
# The hostname is optional: When no hostname is specified,
# at least one ip address needs to be specified.
host_name = "thorium"
# ips for ethernet and wifi
ips = ["192.168.178.189"]
# optional port
port = 4242
> [!Important]
> Gnome -> Sway only partially works (modifier events are not handled correctly)
> [!Important]
> **Wayfire**
>
> If you are using [Wayfire](https://github.com/WayfireWM/wayfire), make sure to use a recent version (must be newer than October 23rd) and **add `shortcuts-inhibit` to the list of plugins in your wayfire config!**
> Otherwise input capture will not work.
## Installation
### Install with cargo
```sh
cargo install lan-mouse
```
Where `left` can be either `left`, `right`, `top` or `bottom`.
### Download from Releases
The easiest way to install Lan Mouse is to download precompiled release binaries from the [releases section](https://github.com/feschber/lan-mouse/releases).
> :warning: Note that clients from the config
> file are currently ignored when using the gtk frontend!
For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies).
### Arch Linux
Lan Mouse is available on the AUR:
```sh
# git version (includes latest changes)
paru -S lan-mouse-git
# alternatively
paru -S lan-mouse-bin
```
### Nix
- nixpkgs: [search.nixos.org](https://search.nixos.org/packages?channel=unstable&show=lan-mouse&from=0&size=50&sort=relevance&type=packages&query=lan-mouse)
- flake: [README.md](./nix/README.md)
## Build and Run
### Building from Source
Build in release mode:
```sh
cargo build --release
@@ -65,115 +95,225 @@ Run directly:
cargo run --release
```
Install the files:
```sh
# install lan-mouse
sudo cp target/release/lan-mouse /usr/local/bin/
# install app icon
sudo mkdir -p /usr/local/share/icons/hicolor/scalable/apps
sudo cp resources/de.feschber.LanMouse.svg /usr/local/share/icons/hicolor/scalable/apps
# update icon cache
gtk-update-icon-cache /usr/local/share/icons/hicolor/
# install desktop entry
sudo mkdir -p /usr/local/share/applications
sudo cp de.feschber.LanMouse.desktop /usr/local/share/applications
# when using firewalld: install firewall rule
sudo cp firewall/lan-mouse.xml /etc/firewalld/services
# -> enable the service in firewalld settings
```
### Conditional Compilation
Currently only x11, wayland and windows are supported backends,
Currently only x11, wayland, windows and MacOS are supported backends.
Depending on the toolchain used, support for other platforms is omitted
automatically (it does not make sense to build a Windows `.exe` with
support for x11 and wayland backends).
However one might still want to omit support for e.g. wayland or x11 on
However one might still want to omit support for e.g. wayland, x11 or libei on
a Linux system.
This is possible through
[cargo features](https://doc.rust-lang.org/cargo/reference/features.html)
[cargo features](https://doc.rust-lang.org/cargo/reference/features.html).
E.g. if only wayland support is needed, the following command produces
an executable with just support for wayland:
```sh
cargo build --no-default-features --features wayland
```
## OS Support
The following table shows support for Event receiving and event Emitting
on different operating systems:
| Backend | Event Receiving | Event Emitting |
|---------------------------|--------------------------|--------------------------------------|
| Wayland (wlroots) | :heavy_check_mark: | :heavy_check_mark: |
| Wayland (KDE) | WIP | :heavy_check_mark: |
| Wayland (Gnome) | TODO (libei support) | TODO (wlr-layer-shell not supported) |
| X11 | WIP | TODO |
| Windows | needs improvements | TODO |
| MacOS | TODO (I dont own a Mac) | TODO (I dont own a Mac) |
## Wayland compositor support
### Input Emulation (for receiving events)
On wayland input-emulation is in an early/unstable state as of writing this.
Different compositors have different ways of enabling input emulation:
Most wlroots-based compositors like Hyprland and Sway support the following
unstable wayland protocols for keyboard and mouse emulation:
- [virtual-keyboard-unstable-v1](https://wayland.app/protocols/virtual-keyboard-unstable-v1)
- [wlr-virtual-pointer-unstable-v1](https://wayland.app/protocols/wlr-virtual-pointer-unstable-v1) are used to emulate input on wlroots compositors
KDE also has a protocol for input emulation ([kde-fake-input](https://wayland.app/protocols/kde-fake-input)), it is however not exposed to
third party apps, so the recommended way of enabling input emulation in KDE is the
[freedesktop remote-desktop-portal](https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.RemoteDesktop).
Gnome uses [libei](https://gitlab.freedesktop.org/libinput/libei) for input emulation,
which has the goal to become the general approach for emulating Input on wayland.
| Required Protocols (Event Receiving) | Sway | Kwin | Gnome |
|----------------------------------------|--------------------|----------------------|----------------------|
| wlr-virtual-pointer-unstable-v1 | :heavy_check_mark: | :x: | :x: |
| virtual-keyboard-unstable-v1 | :heavy_check_mark: | :x: | :x: |
| ~fake-input~ | :x: | ~:heavy_check_mark:~ | :x: |
### Input capture
To capture mouse and keyboard input, a few things are necessary:
- Displaying an immovable surface at screen edges
- Locking the mouse in place
- (optionally but highly recommended) reading unaccelerated mouse input
| Required Protocols (Event Emitting) | Sway | Kwin | Gnome |
|----------------------------------------|--------------------|----------------------|----------------------|
| pointer-constraints-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| relative-pointer-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| keyboard-shortcuts-inhibit-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| wlr-layer-shell-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :x: |
The [zwlr\_virtual\_pointer\_manager\_v1](wlr-virtual-pointer-unstable-v1) is required
to display surfaces on screen edges and used to display the immovable window on
both wlroots based compositors and KDE.
Gnome unfortunately does not support this protocol
and [likely won't ever support it](https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/1141).
So there is currently no way of doing this in Wayland, aside from a custom Gnome-Shell
extension, which is not a very elegant solution.
This is to be looked into in the future.
~In order for layershell surfaces to be able to lock the pointer using the pointer\_constraints protocol [this patch](https://github.com/swaywm/sway/pull/7178) needs to be applied to sway.~
(this works natively on sway versions >= 1.8)
## Windows support
Currently windows can receive mouse and keyboard events,
event producing on windows is WIP.
For a detailed list of available features, checkout the [Cargo.toml](./Cargo.toml)
## TODOS
- [x] Capture the actual mouse events on the server side via a wayland client and send them to the client
- [x] Mouse grabbing
- [x] Window with absolute position -> wlr\_layer\_shell
- [x] DNS resolving
- [x] Keyboard support
- [x] Scrollwheel support
- [x] Button support
- [ ] Latency measurement + logging
- [ ] Bandwidth usage approximation + logging
- [x] Multiple IP addresses -> check which one is reachable
- [x] Merge server and client -> Both client and server can send and receive events depending on what mouse is used where
- [x] Liveness tracking (automatically ungrab mouse when client unreachable)
## Installing Dependencies
#### Macos
```sh
brew install libadwaita
```
#### Ubuntu and derivatives
```sh
sudo apt install libadwaita-1-dev libgtk-4-dev libx11-dev libxtst-dev
```
#### Arch and derivatives
```sh
sudo pacman -S libadwaita gtk libx11 libxtst
```
#### Fedora and derivatives
```sh
sudo dnf install libadwaita-devel libXtst-devel libX11-devel
```
#### Windows
> [!NOTE]
> This is only necessary when building lan-mouse from source. The windows release comes with precompiled gtk dlls.
Follow the instructions at [gtk-rs.org](https://gtk-rs.org/gtk4-rs/stable/latest/book/installation_windows.html)
*TLDR:*
Build gtk from source
- The following commands should be run in an admin power shell instance:
```sh
# install chocolatey
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
# install python 3.11 (Version is important, as 3.12 does not work currently) -> Has been fixed recently
choco install python --version=3.11.0
# install git
choco install git
# install msys2
choco install msys2
# install Visual Studio 2022
choco install visualstudio2022-workload-vctools
```
- The following commands should be run in a regular power shell instance:
```sh
# install gvsbuild with python
python -m pip install --user pipx
python -m pipx ensurepath
```
- Relaunch your powershell instance so the changes in the environment are reflected.
```sh
pipx install gvsbuild
# build gtk + libadwaita
gvsbuild build gtk4 libadwaita librsvg
```
Make sure to add the directory `C:\gtk-build\gtk\x64\release\bin`
[to the `PATH` environment variable]((https://learn.microsoft.com/en-us/previous-versions/office/developer/sharepoint-2010/ee537574(v=office.14))). Otherwise the project will fail to build.
To avoid building GTK from source, it is possible to disable
the gtk frontend (see conditional compilation below).
## Usage
### Gtk Frontend
By default the gtk frontend will open when running `lan-mouse`.
To add a new connection, simply click the `Add` button on *both* devices,
enter the corresponding hostname and activate it.
If the mouse can not be moved onto a device, make sure you have port `4242` (or the one selected)
opened up in your firewall.
### Command Line Interface
The cli interface can be enabled using `--frontend cli` as commandline arguments.
Type `help` to list the available commands.
E.g.:
```sh
$ cargo run --release -- --frontend cli
(...)
> connect <host> left|right|top|bottom
(...)
> list
(...)
> activate 0
```
### Daemon
Lan Mouse can be launched in daemon mode to keep it running in the background.
To do so, add `--daemon` to the commandline args:
```sh
$ cargo run --release -- --daemon
```
In order to start lan-mouse with a graphical session automatically,
the [systemd-service](service/lan-mouse.service) can be used:
Copy the file to `~/.config/systemd/user/` and enable the service:
```sh
cp service/lan-mouse.service ~/.config/systemd/user
systemctl --user daemon-reload
systemctl --user enable --now lan-mouse.service
```
## Configuration
To automatically load clients on startup, the file `$XDG_CONFIG_HOME/lan-mouse/config.toml` is parsed.
`$XDG_CONFIG_HOME` defaults to `~/.config/`.
To create this file you can copy the following example config:
### Example config
> [!TIP]
> key symbols in the release bind are named according
> to their names in [src/scancode.rs#L172](src/scancode.rs#L172).
> This is bound to change
```toml
# example configuration
# configure release bind
release_bind = [ "KeyA", "KeyS", "KeyD", "KeyF" ]
# optional port (defaults to 4242)
port = 4242
# # optional frontend -> defaults to gtk if available
# # possible values are "cli" and "gtk"
# frontend = "gtk"
# define a client on the right side with host name "iridium"
[right]
# hostname
hostname = "iridium"
# activate this client immediately when lan-mouse is started
activate_on_startup = true
# optional list of (known) ip addresses
ips = ["192.168.178.156"]
# define a client on the left side with IP address 192.168.178.189
[left]
# The hostname is optional: When no hostname is specified,
# at least one ip address needs to be specified.
hostname = "thorium"
# ips for ethernet and wifi
ips = ["192.168.178.189", "192.168.178.172"]
# optional port
port = 4242
```
Where `left` can be either `left`, `right`, `top` or `bottom`.
## Roadmap
- [x] Graphical frontend (gtk + libadwaita)
- [x] respect xdg-config-home for config file location.
- [x] IP Address switching
- [x] Liveness tracking Automatically ungrab mouse when client unreachable
- [x] Liveness tracking: Automatically release keys, when server offline
- [x] MacOS KeyCode Translation
- [x] Libei Input Capture
- [ ] X11 Input Capture
- [ ] Windows Input Capture
- [ ] MacOS Input Capture
- [ ] Latency measurement and visualization
- [ ] Bandwidth usage measurement and visualization
- [ ] Clipboard support
- [x] Graphical frontend (gtk?)
- [ ] *Encrytion*
- [ ] Gnome Shell Extension (layer shell is not supported)
- [ ] respect xdg-config-home for config file location.
- [ ] *Encryption*
## Protocol
Currently *all* mouse and keyboard events are sent via **UDP** for performance reasons.
@@ -222,3 +362,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)

View File

@@ -1,6 +1,5 @@
fn main() {
// composite_templates
#[cfg(unix)]
glib_build_tools::compile_resources(
&["resources"],
"resources/resources.gresource.xml",

View File

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

View File

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

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

8
firewall/lan-mouse.xml Normal file
View File

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

82
flake.lock generated Normal file
View File

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

58
flake.nix Normal file
View File

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

40
nix/README.md Normal file
View File

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

42
nix/default.nix Normal file
View File

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

50
nix/hm-module.nix Normal file
View File

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

View File

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -7,6 +7,6 @@
<file compressed="true">style-dark.css</file>
</gresource>
<gresource prefix="/de/feschber/LanMouse/icons">
<file compressed="true" preprocess="xml-stripblanks">mouse-icon.svg</file>
<file compressed="true" preprocess="xml-stripblanks">de.feschber.LanMouse.svg</file>
</gresource>
</gresources>

View File

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

View File

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

View File

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

13
service/lan-mouse.service Normal file
View File

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

1
shell.nix Normal file
View File

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

View File

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

View File

@@ -1,14 +0,0 @@
#[cfg(windows)]
pub mod windows;
#[cfg(all(unix, feature="x11"))]
pub mod x11;
#[cfg(all(unix, feature = "wayland"))]
pub mod wlroots;
#[cfg(all(unix, feature = "xdg_desktop_portal"))]
pub mod xdg_desktop_portal;
#[cfg(all(unix, feature = "libei"))]
pub mod libei;

View File

@@ -1,18 +0,0 @@
use crate::consumer::EventConsumer;
pub struct LibeiConsumer {}
impl LibeiConsumer {
pub fn new() -> Self { Self { } }
}
impl EventConsumer for LibeiConsumer {
fn consume(&self, _: crate::event::Event, _: crate::client::ClientHandle) {
log::error!("libei backend not yet implemented!");
todo!()
}
fn notify(&mut self, _: crate::client::ClientEvent) {
todo!()
}
}

View File

@@ -1,149 +0,0 @@
use crate::{event::{KeyboardEvent, PointerEvent}, consumer::EventConsumer};
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,
},
};
use crate::{
client::{ClientEvent, ClientHandle},
event::Event,
};
pub struct WindowsConsumer {}
impl WindowsConsumer {
pub fn new() -> Self { Self { } }
}
impl EventConsumer for WindowsConsumer {
fn consume(&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 { .. } => {}
},
_ => {}
}
}
fn notify(&mut self, _: ClientEvent) {
// nothing to do
}
}
fn send_mouse_input(mi: MOUSEINPUT) {
unsafe {
let mut input = INPUT {
type_: INPUT_MOUSE,
u: std::mem::transmute(mi),
};
winapi::um::winuser::SendInput(
1 as u32,
&mut input as LPINPUT,
std::mem::size_of::<INPUT>() as i32,
);
}
}
fn rel_mouse(dx: i32, dy: i32) {
let mi = MOUSEINPUT {
dx,
dy,
mouseData: 0,
dwFlags: MOUSEEVENTF_MOVE,
time: 0,
dwExtraInfo: 0,
};
send_mouse_input(mi);
}
fn mouse_button(button: u32, state: u32) {
let dw_flags = match state {
0 => match button {
0x110 => MOUSEEVENTF_LEFTUP,
0x111 => MOUSEEVENTF_RIGHTUP,
0x112 => MOUSEEVENTF_MIDDLEUP,
_ => return
}
1 => match button {
0x110 => MOUSEEVENTF_LEFTDOWN,
0x111 => MOUSEEVENTF_RIGHTDOWN,
0x112 => MOUSEEVENTF_MIDDLEDOWN,
_ => return
}
_ => return
};
let mi = MOUSEINPUT {
dx: 0, dy: 0, // no movement
mouseData: 0,
dwFlags: dw_flags,
time: 0,
dwExtraInfo: 0,
};
send_mouse_input(mi);
}
fn scroll(axis: u8, value: f64) {
let event_type = match axis {
0 => MOUSEEVENTF_WHEEL,
1 => MOUSEEVENTF_HWHEEL,
_ => return
};
let mi = MOUSEINPUT {
dx: 0, dy: 0,
mouseData: (-value * 15.0) as i32 as u32,
dwFlags: event_type,
time: 0,
dwExtraInfo: 0,
};
send_mouse_input(mi);
}
fn key_event(key: u32, state: u8) {
let ki = KEYBDINPUT {
wVk: 0,
wScan: key as u16,
dwFlags: KEYEVENTF_SCANCODE | match state {
0 => KEYEVENTF_KEYUP,
1 => 0u32,
_ => return
},
time: 0,
dwExtraInfo: 0,
};
send_keyboard_input(ki);
}
fn send_keyboard_input(ki: KEYBDINPUT) {
unsafe {
let mut input = INPUT {
type_: INPUT_KEYBOARD,
u: std::mem::zeroed(),
};
*input.u.ki_mut() = ki;
winapi::um::winuser::SendInput(1 as u32, &mut input, std::mem::size_of::<INPUT>() as i32);
}
}

View File

@@ -1,314 +0,0 @@
use wayland_client::WEnum;
use crate::client::{ClientHandle, ClientEvent};
use crate::consumer::EventConsumer;
use std::collections::HashMap;
use std::os::fd::OwnedFd;
use std::os::unix::prelude::AsRawFd;
use anyhow::{Result, anyhow};
use wayland_client::globals::BindError;
use wayland_client::protocol::wl_pointer::{Axis, ButtonState};
use wayland_client::protocol::wl_keyboard::{self, WlKeyboard};
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,
};
use wayland_protocols_misc::zwp_virtual_keyboard_v1::client::{
zwp_virtual_keyboard_manager_v1::ZwpVirtualKeyboardManagerV1 as VkManager,
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},
protocol::{wl_registry, wl_seat},
Connection, Dispatch, EventQueue, QueueHandle,
};
use crate::event::{Event, KeyboardEvent, PointerEvent};
enum VirtualInputManager {
Wlroots { vpm: VpManager, vkm: VkManager },
Kde { fake_input: OrgKdeKwinFakeInput },
}
struct State {
keymap: Option<(u32, OwnedFd, u32)>,
input_for_client: HashMap<ClientHandle, VirtualInput>,
seat: wl_seat::WlSeat,
virtual_input_manager: VirtualInputManager,
qh: QueueHandle<Self>,
}
// App State, implements Dispatch event handlers
pub(crate) struct WlrootsConsumer {
state: State,
queue: EventQueue<State>,
}
impl WlrootsConsumer {
pub fn new() -> Result<Self> {
let conn = Connection::connect_to_env().unwrap();
let (globals, queue) = registry_queue_init::<State>(&conn).unwrap();
let qh = queue.handle();
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: Result<VpManager, BindError> = globals.bind(&qh, 1..=1, ());
let vkm: Result<VkManager, BindError> = globals.bind(&qh, 1..=1, ());
let fake_input: Result<OrgKdeKwinFakeInput, BindError> = globals.bind(&qh, 4..=4, ());
let virtual_input_manager = match (vpm, vkm, fake_input) {
(Ok(vpm), Ok(vkm), _) => VirtualInputManager::Wlroots { vpm, vkm },
(_, _, Ok(fake_input)) => {
fake_input.authenticate(
"lan-mouse".into(),
"Allow remote clients to control this device".into(),
);
VirtualInputManager::Kde { fake_input }
}
(Err(e1), Err(e2), Err(e3)) => {
log::warn!("zwlr_virtual_pointer_v1: {e1}");
log::warn!("zwp_virtual_keyboard_v1: {e2}");
log::warn!("org_kde_kwin_fake_input: {e3}");
log::error!("neither wlroots nor kde input emulation protocol supported!");
return Err(anyhow!("could not create event consumer"));
}
_ => {
panic!()
}
};
let input_for_client: HashMap<ClientHandle, VirtualInput> = HashMap::new();
let mut consumer = WlrootsConsumer {
state: State {
keymap: None,
input_for_client,
seat,
virtual_input_manager,
qh,
},
queue,
};
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)
}
}
impl State {
fn add_client(&mut self, client: ClientHandle) {
// create virtual input devices
match &self.virtual_input_manager {
VirtualInputManager::Wlroots { vpm, vkm } => {
let pointer: Vp = vpm.create_virtual_pointer(None, &self.qh, ());
let keyboard: Vk = vkm.create_virtual_keyboard(&self.seat, &self.qh, ());
// TODO: use server side keymap
if let Some((format, fd, size)) = self.keymap.as_ref() {
keyboard.keymap(*format, fd.as_raw_fd(), *size);
} else {
panic!("no keymap");
}
let vinput = VirtualInput::Wlroots { pointer, keyboard };
self.input_for_client.insert(client, vinput);
}
VirtualInputManager::Kde { fake_input } => {
let fake_input = fake_input.clone();
let vinput = VirtualInput::Kde { fake_input };
self.input_for_client.insert(client, vinput);
}
}
}
}
impl EventConsumer for WlrootsConsumer {
fn consume(&self, event: Event, client_handle: ClientHandle) {
if let Some(virtual_input) = self.state.input_for_client.get(&client_handle) {
virtual_input.consume_event(event).unwrap();
if let Err(e) = self.queue.flush() {
log::error!("{}", e);
}
}
}
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);
}
}
}
}
enum VirtualInput {
Wlroots { pointer: Vp, keyboard: Vk },
Kde { fake_input: OrgKdeKwinFakeInput },
}
impl VirtualInput {
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: _,
} => {
pointer.motion(time, relative_x, relative_y);
}
VirtualInput::Kde { fake_input } => {
fake_input.pointer_motion(relative_y, relative_y);
}
},
PointerEvent::Button {
time,
button,
state,
} => {
let state: ButtonState = state.try_into()?;
match self {
VirtualInput::Wlroots {
pointer,
keyboard: _,
} => {
pointer.button(time, button, state);
}
VirtualInput::Kde { fake_input } => {
fake_input.button(button, state as u32);
}
}
}
PointerEvent::Axis { time, axis, value } => {
let axis: Axis = (axis as u32).try_into()?;
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: _ } => {}
},
},
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::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: _ } => {}
},
},
_ => {},
}
Ok(())
}
}
delegate_noop!(State: Vp);
delegate_noop!(State: Vk);
delegate_noop!(State: VpManager);
delegate_noop!(State: VkManager);
delegate_noop!(State: OrgKdeKwinFakeInput);
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for State {
fn event(
_: &mut State,
_: &wl_registry::WlRegistry,
_: wl_registry::Event,
_: &GlobalListContents,
_: &Connection,
_: &QueueHandle<State>,
) {
}
}
impl Dispatch<WlKeyboard, ()> for State {
fn event(
state: &mut Self,
_: &WlKeyboard,
event: <WlKeyboard as wayland_client::Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
match event {
wl_keyboard::Event::Keymap { format, fd, size } => {
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,57 +0,0 @@
use std::ptr;
use x11::{xlib, xtest};
use crate::{
client::ClientHandle,
event::Event, consumer::EventConsumer,
};
pub struct X11Consumer {
display: *mut xlib::Display,
}
impl X11Consumer {
pub fn new() -> Self {
let display = unsafe {
match xlib::XOpenDisplay(ptr::null()) {
d if d == ptr::null::<xlib::Display>() as *mut xlib::Display => None,
display => Some(display),
}
};
let display = display.expect("could not open display");
Self { display }
}
fn relative_motion(&self, dx: i32, dy: i32) {
unsafe {
xtest::XTestFakeRelativeMotionEvent(self.display, dx, dy, 0, 0);
xlib::XFlush(self.display);
}
}
}
impl EventConsumer for X11Consumer {
fn consume(&self, event: Event, _: ClientHandle) {
match event {
Event::Pointer(pointer_event) => match pointer_event {
crate::event::PointerEvent::Motion {
time: _,
relative_x,
relative_y,
} => {
self.relative_motion(relative_x as i32, relative_y as i32);
}
crate::event::PointerEvent::Button { .. } => {}
crate::event::PointerEvent::Axis { .. } => {}
crate::event::PointerEvent::Frame {} => {}
},
Event::Keyboard(_) => {}
_ => {}
}
}
fn notify(&mut self, _: crate::client::ClientEvent) {
// for our purposes it does not matter what client sent the event
}
}

View File

@@ -1,15 +0,0 @@
use crate::consumer::EventConsumer;
pub struct DesktopPortalConsumer {}
impl DesktopPortalConsumer {
pub fn new() -> Self { Self { } }
}
impl EventConsumer for DesktopPortalConsumer {
fn consume(&self, _: crate::event::Event, _: crate::client::ClientHandle) {
log::error!("xdg_desktop_portal backend not yet implemented!");
}
fn notify(&mut self, _: crate::client::ClientEvent) {}
}

View File

@@ -1,6 +0,0 @@
#[cfg(all(unix, feature = "wayland"))]
pub mod wayland;
#[cfg(windows)]
pub mod windows;
#[cfg(all(unix, feature = "x11"))]
pub mod x11;

View File

@@ -1,58 +0,0 @@
use std::vec::Drain;
use mio::{Token, Registry};
use mio::event::Source;
use std::io::Result;
use crate::{
client::{ClientHandle, ClientEvent},
event::Event,
producer::EventProducer,
};
pub struct WindowsProducer {
pending_events: Vec<(ClientHandle, Event)>,
}
impl Source for WindowsProducer {
fn register(
&mut self,
_registry: &Registry,
_token: Token,
_interests: mio::Interest,
) -> Result<()> {
Ok(())
}
fn reregister(
&mut self,
_registry: &Registry,
_token: Token,
_interests: mio::Interest,
) -> Result<()> {
Ok(())
}
fn deregister(&mut self, _registry: &Registry) -> Result<()> {
Ok(())
}
}
impl EventProducer for WindowsProducer {
fn notify(&mut self, _: ClientEvent) { }
fn read_events(&mut self) -> Drain<(ClientHandle, Event)> {
self.pending_events.drain(..)
}
fn release(&mut self) { }
}
impl WindowsProducer {
pub(crate) fn new() -> Self {
Self {
pending_events: vec![],
}
}
}

View File

@@ -1,55 +0,0 @@
use std::vec::Drain;
use mio::{Token, Registry};
use mio::event::Source;
use std::io::Result;
use crate::producer::EventProducer;
use crate::{client::{ClientHandle, ClientEvent}, event::Event};
pub struct X11Producer {
pending_events: Vec<(ClientHandle, Event)>,
}
impl X11Producer {
pub fn new() -> Self {
Self {
pending_events: vec![],
}
}
}
impl Source for X11Producer {
fn register(
&mut self,
_registry: &Registry,
_token: Token,
_interests: mio::Interest,
) -> Result<()> {
Ok(())
}
fn reregister(
&mut self,
_registry: &Registry,
_token: Token,
_interests: mio::Interest,
) -> Result<()> {
Ok(())
}
fn deregister(&mut self, _registry: &Registry) -> Result<()> {
Ok(())
}
}
impl EventProducer for X11Producer {
fn notify(&mut self, _: ClientEvent) { }
fn read_events(&mut self) -> Drain<(ClientHandle, Event)> {
self.pending_events.drain(..)
}
fn release(&mut self) {}
}

78
src/capture.rs Normal file
View File

@@ -0,0 +1,78 @@
use std::io;
use futures_core::Stream;
use crate::{
client::{ClientEvent, ClientHandle},
event::Event,
};
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
pub mod libei;
#[cfg(target_os = "macos")]
pub mod macos;
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
pub mod wayland;
#[cfg(windows)]
pub mod windows;
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
pub mod x11;
/// fallback input capture (does not produce events)
pub mod dummy;
pub async fn create() -> Box<dyn InputCapture> {
#[cfg(target_os = "macos")]
match macos::MacOSInputCapture::new() {
Ok(p) => return Box::new(p),
Err(e) => log::info!("macos input capture not available: {e}"),
}
#[cfg(windows)]
match windows::WindowsInputCapture::new() {
Ok(p) => return Box::new(p),
Err(e) => log::info!("windows input capture not available: {e}"),
}
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
match libei::LibeiInputCapture::new().await {
Ok(p) => {
log::info!("using libei input capture");
return Box::new(p);
}
Err(e) => log::info!("libei input capture not available: {e}"),
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
match wayland::WaylandInputCapture::new() {
Ok(p) => {
log::info!("using layer-shell input capture");
return Box::new(p);
}
Err(e) => log::info!("layer_shell input capture not available: {e}"),
}
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
match x11::X11InputCapture::new() {
Ok(p) => {
log::info!("using x11 input capture");
return Box::new(p);
}
Err(e) => log::info!("x11 input capture not available: {e}"),
}
log::error!("falling back to dummy input capture");
Box::new(dummy::DummyInputCapture::new())
}
pub trait InputCapture: Stream<Item = io::Result<(ClientHandle, Event)>> + Unpin {
/// notify input capture of configuration changes
fn notify(&mut self, event: ClientEvent) -> io::Result<()>;
/// release mouse
fn release(&mut self) -> io::Result<()>;
}

42
src/capture/dummy.rs Normal file
View File

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

555
src/capture/libei.rs Normal file
View File

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

33
src/capture/macos.rs Normal file
View File

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

View File

@@ -1,9 +1,20 @@
use crate::{client::{ClientHandle, Position, ClientEvent}, producer::EventProducer};
use mio::{event::Source, unix::SourceFd};
use crate::{
capture::InputCapture,
client::{ClientEvent, ClientHandle, Position},
};
use std::{os::fd::RawFd, vec::Drain, io::ErrorKind};
use memmap::MmapOptions;
use anyhow::{anyhow, Result};
use futures_core::Stream;
use memmap::MmapOptions;
use std::{
collections::VecDeque,
env,
io::{self, ErrorKind},
os::fd::{AsFd, OwnedFd, RawFd},
pin::Pin,
task::{ready, Context, Poll},
};
use tokio::io::unix::AsyncFd;
use std::{
fs::File,
@@ -12,20 +23,26 @@ use std::{
rc::Rc,
};
use wayland_protocols::{wp::{
keyboard_shortcuts_inhibit::zv1::client::{
zwp_keyboard_shortcuts_inhibit_manager_v1::ZwpKeyboardShortcutsInhibitManagerV1,
zwp_keyboard_shortcuts_inhibitor_v1::ZwpKeyboardShortcutsInhibitorV1,
use wayland_protocols::{
wp::{
keyboard_shortcuts_inhibit::zv1::client::{
zwp_keyboard_shortcuts_inhibit_manager_v1::ZwpKeyboardShortcutsInhibitManagerV1,
zwp_keyboard_shortcuts_inhibitor_v1::ZwpKeyboardShortcutsInhibitorV1,
},
pointer_constraints::zv1::client::{
zwp_locked_pointer_v1::ZwpLockedPointerV1,
zwp_pointer_constraints_v1::{Lifetime, ZwpPointerConstraintsV1},
},
relative_pointer::zv1::client::{
zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1,
zwp_relative_pointer_v1::{self, ZwpRelativePointerV1},
},
},
pointer_constraints::zv1::client::{
zwp_locked_pointer_v1::ZwpLockedPointerV1,
zwp_pointer_constraints_v1::{Lifetime, ZwpPointerConstraintsV1},
xdg::xdg_output::zv1::client::{
zxdg_output_manager_v1::ZxdgOutputManagerV1,
zxdg_output_v1::{self, ZxdgOutputV1},
},
relative_pointer::zv1::client::{
zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1,
zwp_relative_pointer_v1::{self, ZwpRelativePointerV1},
},
}, xdg::xdg_output::zv1::client::{zxdg_output_manager_v1::ZxdgOutputManagerV1, zxdg_output_v1::{self, ZxdgOutputV1}}};
};
use wayland_protocols_wlr::layer_shell::v1::client::{
zwlr_layer_shell_v1::{Layer, ZwlrLayerShellV1},
@@ -33,14 +50,18 @@ use wayland_protocols_wlr::layer_shell::v1::client::{
};
use wayland_client::{
backend::{WaylandError, ReadEventsGuard},
backend::{ReadEventsGuard, WaylandError},
delegate_noop,
globals::{registry_queue_init, GlobalListContents},
protocol::{
wl_buffer, wl_compositor, wl_keyboard, wl_pointer, wl_region, wl_registry, wl_seat, wl_shm,
wl_shm_pool, wl_surface, wl_output::{self, WlOutput},
wl_buffer, wl_compositor,
wl_keyboard::{self, WlKeyboard},
wl_output::{self, WlOutput},
wl_pointer::{self, WlPointer},
wl_region, wl_registry, wl_seat, wl_shm, wl_shm_pool,
wl_surface::WlSurface,
},
Connection, Dispatch, DispatchError, QueueHandle, WEnum, EventQueue,
Connection, Dispatch, DispatchError, EventQueue, QueueHandle, WEnum,
};
use tempfile;
@@ -55,7 +76,7 @@ struct Globals {
seat: wl_seat::WlSeat,
shm: wl_shm::WlShm,
layer_shell: ZwlrLayerShellV1,
outputs: Vec<wl_output::WlOutput>,
outputs: Vec<WlOutput>,
xdg_output_manager: ZxdgOutputManagerV1,
}
@@ -70,39 +91,57 @@ impl OutputInfo {
fn new() -> Self {
Self {
name: "".to_string(),
position: (0,0),
size: (0,0),
position: (0, 0),
size: (0, 0),
}
}
}
struct State {
pointer: Option<WlPointer>,
keyboard: Option<WlKeyboard>,
pointer_lock: Option<ZwpLockedPointerV1>,
rel_pointer: Option<ZwpRelativePointerV1>,
shortcut_inhibitor: Option<ZwpKeyboardShortcutsInhibitorV1>,
client_for_window: Vec<(Rc<Window>, ClientHandle)>,
focused: Option<(Rc<Window>, ClientHandle)>,
g: Globals,
wayland_fd: RawFd,
wayland_fd: OwnedFd,
read_guard: Option<ReadEventsGuard>,
qh: QueueHandle<Self>,
pending_events: Vec<(ClientHandle, Event)>,
pending_events: VecDeque<(ClientHandle, Event)>,
output_info: Vec<(WlOutput, OutputInfo)>,
}
pub struct WaylandEventProducer {
struct Inner {
state: State,
queue: EventQueue<State>,
}
impl AsRawFd for Inner {
fn as_raw_fd(&self) -> RawFd {
self.state.wayland_fd.as_raw_fd()
}
}
pub struct WaylandInputCapture(AsyncFd<Inner>);
struct Window {
buffer: wl_buffer::WlBuffer,
surface: wl_surface::WlSurface,
surface: WlSurface,
layer_surface: ZwlrLayerSurfaceV1,
pos: Position,
}
impl Window {
fn new(state: &State, qh: &QueueHandle<State>, output: &WlOutput, pos: Position, size: (i32, i32)) -> Window {
fn new(
state: &State,
qh: &QueueHandle<State>,
output: &WlOutput,
pos: Position,
size: (i32, i32),
) -> Window {
log::debug!("creating window output: {output:?}, size: {size:?}");
let g = &state.g;
let (width, height) = match pos {
@@ -113,7 +152,7 @@ impl Window {
draw(&mut file, (width, height));
let pool = g
.shm
.create_pool(file.as_raw_fd(), (width * height * 4) as i32, qh, ());
.create_pool(file.as_fd(), (width * height * 4) as i32, qh, ());
let buffer = pool.create_buffer(
0,
width as i32,
@@ -127,8 +166,8 @@ impl Window {
let layer_surface = g.layer_shell.get_layer_surface(
&surface,
Some(&output),
Layer::Top,
Some(output),
Layer::Overlay,
"LAN Mouse Sharing".into(),
qh,
(),
@@ -142,11 +181,12 @@ impl Window {
layer_surface.set_anchor(anchor);
layer_surface.set_size(width, height);
layer_surface.set_exclusive_zone(0);
layer_surface.set_exclusive_zone(-1);
layer_surface.set_margin(0, 0, 0, 0);
surface.set_input_region(None);
surface.commit();
Window {
pos,
buffer,
surface,
layer_surface,
@@ -164,30 +204,39 @@ impl Drop for Window {
}
fn get_edges(outputs: &[(WlOutput, OutputInfo)], pos: Position) -> Vec<(WlOutput, i32)> {
outputs.iter().map(|(o, i)| {
(o.clone(),
match pos {
Position::Left => i.position.0,
Position::Right => i.position.0 + i.size.0,
Position::Top => i.position.1,
Position::Bottom => i.position.1 + i.size.1,
outputs
.iter()
.map(|(o, i)| {
(
o.clone(),
match pos {
Position::Left => i.position.0,
Position::Right => i.position.0 + i.size.0,
Position::Top => i.position.1,
Position::Bottom => i.position.1 + i.size.1,
},
)
})
}).collect()
.collect()
}
fn get_output_configuration(state: &State, pos: Position) -> Vec<(WlOutput, OutputInfo)> {
// get all output edges corresponding to the position
let edges = get_edges(&state.output_info, pos);
log::debug!("edges: {edges:?}");
let opposite_edges = get_edges(&state.output_info, pos.opposite());
// remove those edges that are at the same position
// as an opposite edge of a different output
let outputs: Vec<WlOutput> = edges.iter().filter(|(_,edge)| {
opposite_edges.iter().map(|(_,e)| *e).find(|e| e == edge).is_none()
}).map(|(o,_)| o.clone()).collect();
state.output_info
let outputs: Vec<WlOutput> = edges
.iter()
.filter(|(o,_)| outputs.contains(o))
.filter(|(_, edge)| !opposite_edges.iter().map(|(_, e)| *e).any(|e| &e == edge))
.map(|(o, _)| o.clone())
.collect();
state
.output_info
.iter()
.filter(|(o, _)| outputs.contains(o))
.map(|(o, i)| (o.clone(), i.clone()))
.collect()
}
@@ -196,12 +245,18 @@ fn draw(f: &mut File, (width, height): (u32, u32)) {
let mut buf = BufWriter::new(f);
for _ in 0..height {
for _ in 0..width {
buf.write_all(&0x44FbF1C7u32.to_ne_bytes()).unwrap();
if env::var("LM_DEBUG_LAYER_SHELL").ok().is_some() {
// AARRGGBB
buf.write_all(&0xff11d116u32.to_ne_bytes()).unwrap();
} else {
// AARRGGBB
buf.write_all(&0x00000000u32.to_ne_bytes()).unwrap();
}
}
}
}
impl WaylandEventProducer {
impl WaylandInputCapture {
pub fn new() -> Result<Self> {
let conn = match Connection::connect_to_env() {
Ok(c) => c,
@@ -250,10 +305,15 @@ impl WaylandEventProducer {
Err(_) => return Err(anyhow!("zwp_relative_pointer_manager_v1 not supported")),
};
let shortcut_inhibit_manager: ZwpKeyboardShortcutsInhibitManagerV1 = match g.bind(&qh, 1..=1, ()) {
Ok(shortcut_inhibit_manager) => shortcut_inhibit_manager,
Err(_) => return Err(anyhow!("zwp_keyboard_shortcuts_inhibit_manager_v1 not supported")),
};
let shortcut_inhibit_manager: ZwpKeyboardShortcutsInhibitManagerV1 =
match g.bind(&qh, 1..=1, ()) {
Ok(shortcut_inhibit_manager) => shortcut_inhibit_manager,
Err(_) => {
return Err(anyhow!(
"zwp_keyboard_shortcuts_inhibit_manager_v1 not supported"
))
}
};
let outputs = vec![];
@@ -273,11 +333,13 @@ impl WaylandEventProducer {
queue.flush()?;
// prepare reading wayland events
let read_guard = queue.prepare_read()?;
let wayland_fd = read_guard.connection_fd().as_raw_fd();
let read_guard = queue.prepare_read().unwrap(); // there can not yet be events to dispatch
let wayland_fd = read_guard.connection_fd().try_clone_to_owned().unwrap();
std::mem::drop(read_guard);
let mut state = State {
pointer: None,
keyboard: None,
g,
pointer_lock: None,
rel_pointer: None,
@@ -287,7 +349,7 @@ impl WaylandEventProducer {
qh,
wayland_fd,
read_guard: None,
pending_events: vec![],
pending_events: VecDeque::new(),
output_info: vec![],
};
@@ -301,7 +363,10 @@ impl WaylandEventProducer {
// read outputs
for output in state.g.outputs.iter() {
state.g.xdg_output_manager.get_xdg_output(output, &state.qh, output.clone());
state
.g
.xdg_output_manager
.get_xdg_output(output, &state.qh, output.clone());
}
// roundtrip to read xdg_output events
@@ -312,18 +377,46 @@ impl WaylandEventProducer {
log::debug!("{:#?}", i.1);
}
let read_guard = queue.prepare_read()?;
let read_guard = loop {
match queue.prepare_read() {
Some(r) => break r,
None => {
queue.dispatch_pending(&mut state)?;
continue;
}
}
};
state.read_guard = Some(read_guard);
Ok(WaylandEventProducer { queue, state })
let inner = AsyncFd::new(Inner { queue, state })?;
Ok(WaylandInputCapture(inner))
}
fn add_client(&mut self, handle: ClientHandle, pos: Position) {
self.0.get_mut().state.add_client(handle, pos);
}
fn delete_client(&mut self, handle: ClientHandle) {
let inner = self.0.get_mut();
// remove all windows corresponding to this client
while let Some(i) = inner
.state
.client_for_window
.iter()
.position(|(_, c)| *c == handle)
{
inner.state.client_for_window.remove(i);
inner.state.focused = None;
}
}
}
impl State {
fn grab(
&mut self,
surface: &wl_surface::WlSurface,
pointer: &wl_pointer::WlPointer,
surface: &WlSurface,
pointer: &WlPointer,
serial: u32,
qh: &QueueHandle<State>,
) {
@@ -403,42 +496,31 @@ impl State {
}
fn add_client(&mut self, client: ClientHandle, pos: Position) {
let outputs = get_output_configuration(self, pos);
log::debug!("outputs: {outputs:?}");
outputs.iter().for_each(|(o, i)| {
let window = Window::new(&self, &self.qh, &o, pos, i.size);
let window = Window::new(self, &self.qh, o, pos, i.size);
let window = Rc::new(window);
self.client_for_window.push((window, client));
});
}
fn update_windows(&mut self) {
log::debug!("updating windows");
log::debug!("output info: {:?}", self.output_info);
let clients: Vec<_> = self
.client_for_window
.drain(..)
.map(|(w, c)| (c, w.pos))
.collect();
for (client, pos) in clients {
self.add_client(client, pos);
}
}
}
impl Source for WaylandEventProducer {
fn register(
&mut self,
registry: &mio::Registry,
token: mio::Token,
interests: mio::Interest,
) -> std::io::Result<()> {
SourceFd(&self.state.wayland_fd).register(registry, token, interests)
}
fn reregister(
&mut self,
registry: &mio::Registry,
token: mio::Token,
interests: mio::Interest,
) -> std::io::Result<()> {
SourceFd(&self.state.wayland_fd).reregister(registry, token, interests)
}
fn deregister(&mut self, registry: &mio::Registry) -> std::io::Result<()> {
SourceFd(&self.state.wayland_fd).deregister(registry)
}
}
impl WaylandEventProducer {
impl Inner {
fn read(&mut self) -> bool {
match self.state.read_guard.take().unwrap().read() {
Ok(_) => true,
@@ -453,16 +535,20 @@ impl WaylandEventProducer {
}
}
fn prepare_read(&mut self) {
match self.queue.prepare_read() {
Ok(r) => self.state.read_guard = Some(r),
Err(WaylandError::Io(e)) => {
log::error!("error preparing read from wayland socket: {e}")
fn prepare_read(&mut self) -> io::Result<()> {
loop {
match self.queue.prepare_read() {
None => match self.queue.dispatch_pending(&mut self.state) {
Ok(_) => continue,
Err(DispatchError::Backend(WaylandError::Io(e))) => return Err(e),
Err(e) => panic!("failed to dispatch wayland events: {e}"),
},
Some(r) => {
self.state.read_guard = Some(r);
break Ok(());
}
}
Err(WaylandError::Protocol(e)) => {
panic!("wayland Protocol violation: {e}")
}
};
}
}
fn dispatch_events(&mut self) {
@@ -484,70 +570,101 @@ impl WaylandEventProducer {
}
}
fn flush_events(&mut self) {
fn flush_events(&mut self) -> io::Result<()> {
// flush outgoing events
match self.queue.flush() {
Ok(_) => (),
Err(e) => match e {
WaylandError::Io(e) => {
log::error!("error writing to wayland socket: {e}")
},
return Err(e);
}
WaylandError::Protocol(e) => {
panic!("wayland protocol violation: {e}")
},
}
},
}
Ok(())
}
}
impl EventProducer for WaylandEventProducer {
fn read_events(&mut self) -> Drain<(ClientHandle, Event)> {
// read events
while self.read() {
// prepare next read
self.prepare_read();
}
// dispatch the events
self.dispatch_events();
// flush outgoing events
self.flush_events();
// prepare for the next read
self.prepare_read();
// return the events
self.state.pending_events.drain(..)
}
fn notify(&mut self, client_event: ClientEvent) {
if let ClientEvent::Create(handle, pos) = client_event {
self.state.add_client(handle, pos);
}
if let ClientEvent::Destroy(handle) = client_event {
loop {
// remove all windows corresponding to this client
if let Some(i) = self.state.client_for_window.iter().position(|(_,c)| *c == handle) {
self.state.client_for_window.remove(i);
self.state.focused = None;
} else {
break
}
impl InputCapture for WaylandInputCapture {
fn notify(&mut self, client_event: ClientEvent) -> io::Result<()> {
match client_event {
ClientEvent::Create(handle, pos) => {
self.add_client(handle, pos);
}
ClientEvent::Destroy(handle) => {
self.delete_client(handle);
}
}
self.flush_events();
let inner = self.0.get_mut();
inner.flush_events()
}
fn release(&mut self) {
self.state.ungrab();
self.flush_events();
fn release(&mut self) -> io::Result<()> {
log::debug!("releasing pointer");
let inner = self.0.get_mut();
inner.state.ungrab();
inner.flush_events()
}
}
impl Stream for WaylandInputCapture {
type Item = io::Result<(ClientHandle, Event)>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
if let Some(event) = self.0.get_mut().state.pending_events.pop_front() {
return Poll::Ready(Some(Ok(event)));
}
loop {
let mut guard = ready!(self.0.poll_read_ready_mut(cx))?;
{
let inner = guard.get_inner_mut();
// read events
while inner.read() {
// prepare next read
match inner.prepare_read() {
Ok(_) => {}
Err(e) => return Poll::Ready(Some(Err(e))),
}
}
// dispatch the events
inner.dispatch_events();
// flush outgoing events
if let Err(e) = inner.flush_events() {
if e.kind() != ErrorKind::WouldBlock {
return Poll::Ready(Some(Err(e)));
}
}
// prepare for the next read
match inner.prepare_read() {
Ok(_) => {}
Err(e) => return Poll::Ready(Some(Err(e))),
}
}
// clear read readiness for tokio read guard
// guard.clear_ready_matching(Ready::READABLE);
guard.clear_ready();
// if an event has been queued during dispatch_events() we return it
match guard.get_inner_mut().state.pending_events.pop_front() {
Some(event) => return Poll::Ready(Some(Ok(event))),
None => continue,
}
}
}
}
impl Dispatch<wl_seat::WlSeat, ()> for State {
fn event(
_: &mut Self,
state: &mut Self,
seat: &wl_seat::WlSeat,
event: <wl_seat::WlSeat as wayland_client::Proxy>::Event,
_: &(),
@@ -558,26 +675,25 @@ impl Dispatch<wl_seat::WlSeat, ()> for State {
capabilities: WEnum::Value(capabilities),
} = event
{
if capabilities.contains(wl_seat::Capability::Pointer) {
seat.get_pointer(qh, ());
if capabilities.contains(wl_seat::Capability::Pointer) && state.pointer.is_none() {
state.pointer.replace(seat.get_pointer(qh, ()));
}
if capabilities.contains(wl_seat::Capability::Keyboard) {
if capabilities.contains(wl_seat::Capability::Keyboard) && state.keyboard.is_none() {
seat.get_keyboard(qh, ());
}
}
}
}
impl Dispatch<wl_pointer::WlPointer, ()> for State {
impl Dispatch<WlPointer, ()> for State {
fn event(
app: &mut Self,
pointer: &wl_pointer::WlPointer,
event: <wl_pointer::WlPointer as wayland_client::Proxy>::Event,
pointer: &WlPointer,
event: <WlPointer as wayland_client::Proxy>::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
match event {
wl_pointer::Event::Enter {
serial,
@@ -586,14 +702,14 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
surface_y: _,
} => {
// get client corresponding to the focused surface
log::trace!("produce: enter()");
{
if let Some((window, client)) = app
.client_for_window
.iter()
.find(|(w, _c)| w.surface == surface) {
.find(|(w, _c)| w.surface == surface)
{
app.focused = Some((window.clone(), *client));
app.grab(&surface, pointer, serial.clone(), qh);
app.grab(&surface, pointer, serial, qh);
} else {
return;
}
@@ -603,10 +719,19 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
.iter()
.find(|(w, _c)| w.surface == surface)
.unwrap();
app.pending_events.push((*client, Event::Release()));
app.pending_events.push_back((*client, Event::Enter()));
}
wl_pointer::Event::Leave { .. } => {
log::trace!("produce: leave()");
/* There are rare cases, where when a window is opened in
* just the wrong moment, the pointer is released, while
* still grabbed.
* In that case, the pointer must be ungrabbed, otherwise
* it is impossible to grab it again (since the pointer
* lock, relative pointer,... objects are still in place)
*/
if app.pointer_lock.is_some() {
log::warn!("compositor released mouse");
}
app.ungrab();
}
wl_pointer::Event::Button {
@@ -615,9 +740,8 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
button,
state,
} => {
log::trace!("produce: button()");
let (_, client) = app.focused.as_ref().unwrap();
app.pending_events.push((
app.pending_events.push_back((
*client,
Event::Pointer(PointerEvent::Button {
time,
@@ -627,9 +751,8 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
));
}
wl_pointer::Event::Axis { time, axis, value } => {
log::trace!("produce: scroll()");
let (_, client) = app.focused.as_ref().unwrap();
app.pending_events.push((
app.pending_events.push_back((
*client,
Event::Pointer(PointerEvent::Axis {
time,
@@ -639,22 +762,19 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
));
}
wl_pointer::Event::Frame {} => {
log::trace!("produce: frame()");
let (_, client) = app.focused.as_ref().unwrap();
app.pending_events.push((
*client,
Event::Pointer(PointerEvent::Frame {}),
));
// TODO properly handle frame events
// we simply insert a frame event on the client side
// after each event for now
}
_ => {}
}
}
}
impl Dispatch<wl_keyboard::WlKeyboard, ()> for State {
impl Dispatch<WlKeyboard, ()> for State {
fn event(
app: &mut Self,
_: &wl_keyboard::WlKeyboard,
_: &WlKeyboard,
event: wl_keyboard::Event,
_: &(),
_: &Connection,
@@ -672,7 +792,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for State {
state,
} => {
if let Some(client) = client {
app.pending_events.push((
app.pending_events.push_back((
*client,
Event::Keyboard(KeyboardEvent::Key {
time,
@@ -690,7 +810,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for State {
group,
} => {
if let Some(client) = client {
app.pending_events.push((
app.pending_events.push_back((
*client,
Event::Keyboard(KeyboardEvent::Modifiers {
mods_depressed,
@@ -700,10 +820,6 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for State {
}),
));
}
if mods_depressed == 77 {
// ctrl shift super alt
app.ungrab();
}
}
wl_keyboard::Event::Keymap {
format: _,
@@ -737,10 +853,9 @@ impl Dispatch<ZwpRelativePointerV1, ()> for State {
dy_unaccel: surface_y,
} = event
{
log::trace!("produce: motion()");
if let Some((_window, client)) = &app.focused {
let time = (((utime_hi as u64) << 32 | utime_lo as u64) / 1000) as u32;
app.pending_events.push((
app.pending_events.push_back((
*client,
Event::Pointer(PointerEvent::Motion {
time,
@@ -766,11 +881,12 @@ impl Dispatch<ZwlrLayerSurfaceV1, ()> for State {
if let Some((window, _client)) = app
.client_for_window
.iter()
.find(|(w, _c)| &w.layer_surface == layer_surface) {
.find(|(w, _c)| &w.layer_surface == layer_surface)
{
// client corresponding to the layer_surface
let surface = &window.surface;
let buffer = &window.buffer;
surface.attach(Some(&buffer), 0, 0);
surface.attach(Some(buffer), 0, 0);
layer_surface.ack_configure(serial);
surface.commit();
}
@@ -801,17 +917,21 @@ impl Dispatch<wl_registry::WlRegistry, ()> for State {
qh: &QueueHandle<Self>,
) {
match event {
wl_registry::Event::Global { name, interface, version: _ } => {
match interface.as_str() {
"wl_output" => {
log::debug!("wl_output global");
state.g.outputs.push(registry.bind::<wl_output::WlOutput, _, _>(name, 4, qh, ()))
}
_ => {}
wl_registry::Event::Global {
name,
interface,
version: _,
} => {
if interface.as_str() == "wl_output" {
log::debug!("wl_output global");
state
.g
.outputs
.push(registry.bind::<WlOutput, _, _>(name, 4, qh, ()))
}
},
wl_registry::Event::GlobalRemove { .. } => {},
_ => {},
}
wl_registry::Event::GlobalRemove { .. } => {}
_ => {}
}
}
}
@@ -826,11 +946,8 @@ impl Dispatch<ZxdgOutputV1, WlOutput> for State {
_: &QueueHandle<Self>,
) {
log::debug!("xdg-output - {event:?}");
let output_info = match state
.output_info
.iter_mut()
.find(|(o, _)| o == wl_output) {
Some((_,c)) => c,
let output_info = match state.output_info.iter_mut().find(|(o, _)| o == wl_output) {
Some((_, c)) => c,
None => {
let output_info = OutputInfo::new();
state.output_info.push((wl_output.clone(), output_info));
@@ -841,16 +958,31 @@ impl Dispatch<ZxdgOutputV1, WlOutput> for State {
match event {
zxdg_output_v1::Event::LogicalPosition { x, y } => {
output_info.position = (x, y);
},
}
zxdg_output_v1::Event::LogicalSize { width, height } => {
output_info.size = (width, height);
},
zxdg_output_v1::Event::Done => {},
}
zxdg_output_v1::Event::Done => {}
zxdg_output_v1::Event::Name { name } => {
output_info.name = name;
},
zxdg_output_v1::Event::Description { .. } => {},
_ => {},
}
zxdg_output_v1::Event::Description { .. } => {}
_ => {}
}
}
}
impl Dispatch<WlOutput, ()> for State {
fn event(
state: &mut Self,
_proxy: &WlOutput,
event: <WlOutput as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
if let wl_output::Event::Done = event {
state.update_windows();
}
}
}
@@ -865,10 +997,9 @@ delegate_noop!(State: ZwpKeyboardShortcutsInhibitManagerV1);
delegate_noop!(State: ZwpPointerConstraintsV1);
// ignore events
delegate_noop!(State: ignore wl_output::WlOutput);
delegate_noop!(State: ignore ZxdgOutputManagerV1);
delegate_noop!(State: ignore wl_shm::WlShm);
delegate_noop!(State: ignore wl_buffer::WlBuffer);
delegate_noop!(State: ignore wl_surface::WlSurface);
delegate_noop!(State: ignore WlSurface);
delegate_noop!(State: ignore ZwpKeyboardShortcutsInhibitorV1);
delegate_noop!(State: ignore ZwpLockedPointerV1);

35
src/capture/windows.rs Normal file
View File

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

39
src/capture/x11.rs Normal file
View File

@@ -0,0 +1,39 @@
use anyhow::{anyhow, Result};
use std::io;
use std::task::Poll;
use futures_core::Stream;
use crate::capture::InputCapture;
use crate::event::Event;
use crate::client::{ClientEvent, ClientHandle};
pub struct X11InputCapture {}
impl X11InputCapture {
pub fn new() -> Result<Self> {
Err(anyhow!("not implemented"))
}
}
impl InputCapture for X11InputCapture {
fn notify(&mut self, _event: ClientEvent) -> io::Result<()> {
Ok(())
}
fn release(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Stream for X11InputCapture {
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,6 +1,10 @@
use std::{net::SocketAddr, collections::{HashSet, hash_set::Iter}, fmt::Display, time::{Instant, Duration}, iter::Cloned};
use std::{
collections::HashSet,
fmt::Display,
net::{IpAddr, SocketAddr},
};
use serde::{Serialize, Deserialize};
use serde::{Deserialize, Serialize};
#[derive(Debug, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)]
pub enum Position {
@@ -10,6 +14,12 @@ pub enum Position {
Bottom,
}
impl Default for Position {
fn default() -> Self {
Self::Left
}
}
impl Position {
pub fn opposite(&self) -> Self {
match self {
@@ -23,189 +33,184 @@ impl Position {
impl Display for Position {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", match self {
Position::Left => "left",
Position::Right => "right",
Position::Top => "top",
Position::Bottom => "bottom",
})
write!(
f,
"{}",
match self {
Position::Left => "left",
Position::Right => "right",
Position::Top => "top",
Position::Bottom => "bottom",
}
)
}
}
impl TryFrom<&str> for Position {
type Error = ();
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
"left" => Ok(Position::Left),
"right" => Ok(Position::Right),
"top" => Ok(Position::Top),
"bottom" => Ok(Position::Bottom),
_ => Err(()),
}
}
}
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub struct Client {
/// handle to refer to the client.
/// This way any event consumer / producer backend does not
/// 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 emulation / capture backend does not
/// need to know anything about a client other than its handle.
pub handle: ClientHandle,
/// `active` address of the client, used to send data to.
/// This should generally be the socket address where data
/// was last received from.
pub active_addr: Option<SocketAddr>,
/// all socket addresses associated with a particular client
/// all ip addresses associated with a particular client
/// e.g. Laptops usually have at least an ethernet and a wifi port
/// which have different ip addresses
pub addrs: HashSet<SocketAddr>,
pub ips: HashSet<IpAddr>,
/// both active_addr and addrs can be None / empty so port needs to be stored seperately
pub port: u16,
/// position of a client on screen
pub pos: Position,
}
#[derive(Clone, Copy, Debug)]
pub enum ClientEvent {
Create(ClientHandle, Position),
Destroy(ClientHandle),
UpdatePos(ClientHandle, Position),
AddAddr(ClientHandle, SocketAddr),
RemoveAddr(ClientHandle, SocketAddr),
}
pub type ClientHandle = u32;
#[derive(Debug, Clone)]
pub struct ClientState {
/// information about the client
pub client: Client,
/// events should be sent to and received from the client
pub active: bool,
/// `active` address of the client, used to send data to.
/// This should generally be the socket address where data
/// was last received from.
pub active_addr: Option<SocketAddr>,
/// tracks whether or not the client is responding to pings
pub alive: bool,
/// keys currently pressed by this client
pub pressed_keys: HashSet<u32>,
}
pub struct ClientManager {
/// probably not beneficial to use a hashmap here
clients: Vec<Client>,
last_ping: Vec<(ClientHandle, Option<Instant>)>,
last_seen: Vec<(ClientHandle, Option<Instant>)>,
last_replied: Vec<(ClientHandle, Option<Instant>)>,
next_client_id: u32,
clients: Vec<Option<ClientState>>, // HashMap likely not beneficial
}
impl Default for ClientManager {
fn default() -> Self {
Self::new()
}
}
impl ClientManager {
pub fn new() -> Self {
Self {
clients: vec![],
next_client_id: 0,
last_ping: vec![],
last_seen: vec![],
last_replied: vec![],
}
Self { clients: vec![] }
}
/// add a new client to this manager
pub fn add_client(&mut self, addrs: HashSet<SocketAddr>, pos: Position) -> ClientHandle {
let handle = self.next_id();
// we dont know, which IP is initially active
let active_addr = None;
pub fn add_client(
&mut self,
hostname: Option<String>,
ips: HashSet<IpAddr>,
port: u16,
pos: Position,
active: bool,
) -> ClientHandle {
// get a new client_handle
let handle = self.free_id();
// store fix ip addresses
let fix_ips = ips.iter().cloned().collect();
// store the client
let client = Client { handle, active_addr, addrs, pos };
self.clients.push(client);
self.last_ping.push((handle, None));
self.last_seen.push((handle, None));
self.last_replied.push((handle, None));
let client = Client {
hostname,
fix_ips,
handle,
ips,
port,
pos,
};
// client was never seen, nor pinged
let client_state = ClientState {
client,
active,
active_addr: None,
alive: false,
pressed_keys: HashSet::new(),
};
if handle as usize >= self.clients.len() {
assert_eq!(handle as usize, self.clients.len());
self.clients.push(Some(client_state));
} else {
self.clients[handle as usize] = Some(client_state);
}
handle
}
/// add a socket address to the given client
pub fn add_addr(&mut self, client: ClientHandle, addr: SocketAddr) {
if let Some(client) = self.get_mut(client) {
client.addrs.insert(addr);
}
}
/// remove socket address from the given client
pub fn remove_addr(&mut self, client: ClientHandle, addr: SocketAddr) {
if let Some(client) = self.get_mut(client) {
client.addrs.remove(&addr);
}
}
pub fn set_default_addr(&mut self, client: ClientHandle, addr: SocketAddr) {
if let Some(client) = self.get_mut(client) {
client.active_addr = Some(addr)
}
}
/// update the position of a client
pub fn update_pos(&mut self, client: ClientHandle, pos: Position) {
if let Some(client) = self.get_mut(client) {
client.pos = pos;
}
}
pub fn get_active_addr(&self, client: ClientHandle) -> Option<SocketAddr> {
self.get(client)?.active_addr
}
pub fn get_addrs(&self, client: ClientHandle) -> Option<Cloned<Iter<'_, SocketAddr>>> {
Some(self.get(client)?.addrs.iter().cloned())
}
pub fn last_ping(&self, client: ClientHandle) -> Option<Duration> {
let last_ping = self.last_ping
.iter()
.find(|(c,_)| *c == client)?.1;
last_ping.map(|p| p.elapsed())
}
pub fn last_seen(&self, client: ClientHandle) -> Option<Duration> {
let last_seen = self.last_seen
.iter()
.find(|(c, _)| *c == client)?.1;
last_seen.map(|t| t.elapsed())
}
pub fn last_replied(&self, client: ClientHandle) -> Option<Duration> {
let last_replied = self.last_replied
.iter()
.find(|(c, _)| *c == client)?.1;
last_replied.map(|t| t.elapsed())
}
pub fn reset_last_ping(&mut self, client: ClientHandle) {
if let Some(c) = self.last_ping
.iter_mut()
.find(|(c, _)| *c == client) {
c.1 = Some(Instant::now());
}
}
pub fn reset_last_seen(&mut self, client: ClientHandle) {
if let Some(c) = self.last_seen
.iter_mut()
.find(|(c, _)| *c == client) {
c.1 = Some(Instant::now());
}
}
pub fn reset_last_replied(&mut self, client: ClientHandle) {
if let Some(c) = self.last_replied
.iter_mut()
.find(|(c, _)| *c == client) {
c.1 = Some(Instant::now());
}
}
/// find a client by its address
pub fn get_client(&self, addr: SocketAddr) -> Option<ClientHandle> {
// since there shouldn't be more than a handful of clients at any given
// time this is likely faster than using a HashMap
self.clients
.iter()
.find(|c| c.addrs.contains(&addr))
.map(|c| c.handle)
.position(|c| {
if let Some(c) = c {
c.active && c.client.ips.contains(&addr.ip())
} else {
false
}
})
.map(|p| p as ClientHandle)
}
pub fn remove_client(&mut self, client: ClientHandle) {
if let Some(i) = self.clients.iter().position(|c| c.handle == client) {
self.clients.remove(i);
self.last_ping.remove(i);
self.last_seen.remove(i);
self.last_replied.remove(i);
/// 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()
}
/// get a free slot in the client list
fn free_id(&mut self) -> ClientHandle {
for i in 0..u32::MAX {
if self.clients.get(i as usize).is_none()
|| self.clients.get(i as usize).unwrap().is_none()
{
return i;
}
}
panic!("Out of client ids");
}
fn next_id(&mut self) -> ClientHandle {
let handle = self.next_client_id;
self.next_client_id += 1;
handle
// 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()
}
fn get<'a>(&'a self, client: ClientHandle) -> Option<&'a Client> {
self.clients
.iter()
.find(|c| c.handle == client)
/// 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()
}
fn get_mut<'a>(&'a mut self, client: ClientHandle) -> Option<&'a mut Client> {
self.clients
.iter_mut()
.find(|c| c.handle == client)
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,14 +1,15 @@
use anyhow::Result;
use clap::Parser;
use serde::{Deserialize, Serialize};
use core::fmt;
use std::collections::HashSet;
use std::net::{IpAddr, SocketAddr};
use std::{error::Error, fs};
use std::env;
use std::net::IpAddr;
use std::{error::Error, fs};
use toml;
use crate::client::Position;
use crate::dns;
use crate::scancode;
use crate::scancode::Linux::{KeyLeftAlt, KeyLeftCtrl, KeyLeftMeta, KeyLeftShift};
pub const DEFAULT_PORT: u16 = 4242;
@@ -16,79 +17,107 @@ pub const DEFAULT_PORT: u16 = 4242;
pub struct ConfigToml {
pub port: Option<u16>,
pub frontend: Option<String>,
pub left: Option<Client>,
pub right: Option<Client>,
pub top: Option<Client>,
pub bottom: Option<Client>,
pub release_bind: Option<Vec<scancode::Linux>>,
pub left: Option<TomlClient>,
pub right: Option<TomlClient>,
pub top: Option<TomlClient>,
pub bottom: Option<TomlClient>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Client {
pub struct TomlClient {
pub hostname: Option<String>,
pub host_name: Option<String>,
pub ips: Option<Vec<IpAddr>>,
pub port: Option<u16>,
pub activate_on_startup: Option<bool>,
}
#[derive(Debug, Clone)]
struct MissingParameter {
arg: &'static str,
}
impl fmt::Display for MissingParameter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Missing a parameter for argument: {}", self.arg)
}
}
impl Error for MissingParameter {}
impl ConfigToml {
pub fn new(path: &str) -> Result<ConfigToml, Box<dyn Error>> {
let config = fs::read_to_string(path)?;
log::info!("using config: \"{path}\"");
Ok(toml::from_str::<_>(&config)?)
}
}
fn find_arg(key: &'static str) -> Result<Option<String>, MissingParameter> {
let args: Vec<String> = env::args().collect();
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct CliArgs {
/// the listen port for lan-mouse
#[arg(short, long)]
port: Option<u16>,
for (i, arg) in args.iter().enumerate() {
if arg != key {
continue;
}
match args.get(i+1) {
None => return Err(MissingParameter { arg: key }),
Some(arg) => return Ok(Some(arg.clone())),
};
}
Ok(None)
/// the frontend to use [cli | gtk]
#[arg(short, long)]
frontend: Option<String>,
/// non-default config file location
#[arg(short, long)]
config: Option<String>,
/// run only the service as a daemon without the frontend
#[arg(short, long)]
daemon: bool,
}
#[derive(PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq)]
pub enum Frontend {
Gtk,
Cli,
}
#[derive(Debug)]
pub struct Config {
pub frontend: Frontend,
pub port: u16,
pub clients: Vec<(Client, Position)>,
pub clients: Vec<(TomlClient, Position)>,
pub daemon: bool,
pub release_bind: Vec<scancode::Linux>,
}
pub struct ConfigClient {
pub ips: HashSet<IpAddr>,
pub hostname: Option<String>,
pub port: u16,
pub pos: Position,
pub active: bool,
}
const DEFAULT_RELEASE_KEYS: [scancode::Linux; 4] =
[KeyLeftCtrl, KeyLeftShift, KeyLeftMeta, KeyLeftAlt];
impl Config {
pub fn new() -> Result<Self, Box<dyn Error>> {
let config_path = "config.toml";
let config_toml = match ConfigToml::new(config_path) {
pub fn new() -> Result<Self> {
let args = CliArgs::parse();
let config_file = "config.toml";
#[cfg(unix)]
let config_path = {
let xdg_config_home =
env::var("XDG_CONFIG_HOME").unwrap_or(format!("{}/.config", env::var("HOME")?));
format!("{xdg_config_home}/lan-mouse/{config_file}")
};
#[cfg(not(unix))]
let config_path = {
let app_data =
env::var("LOCALAPPDATA").unwrap_or(format!("{}/.config", env::var("USERPROFILE")?));
format!("{app_data}\\lan-mouse\\{config_file}")
};
// --config <file> overrules default location
let config_path = args.config.unwrap_or(config_path);
let config_toml = match ConfigToml::new(config_path.as_str()) {
Err(e) => {
log::error!("config.toml: {e}");
log::warn!("{config_path}: {e}");
log::warn!("Continuing without config file ...");
None
},
}
Ok(c) => Some(c),
};
let frontend = match find_arg("--frontend")? {
let frontend = match args.frontend {
None => match &config_toml {
Some(c) => c.frontend.clone(),
None => None,
@@ -97,26 +126,32 @@ impl Config {
};
let frontend = match frontend {
#[cfg(all(unix, feature = "gtk"))]
#[cfg(feature = "gtk")]
None => Frontend::Gtk,
#[cfg(any(not(feature = "gtk"), not(unix)))]
#[cfg(not(feature = "gtk"))]
None => Frontend::Cli,
Some(s) => match s.as_str() {
"cli" => Frontend::Cli,
"gtk" => Frontend::Gtk,
_ => Frontend::Cli,
}
_ => Frontend::Cli,
},
};
let port = match find_arg("--port")? {
Some(port) => port.parse::<u16>()?,
let port = match args.port {
Some(port) => port,
None => match &config_toml {
Some(c) => c.port.unwrap_or(DEFAULT_PORT),
None => DEFAULT_PORT,
}
},
};
let mut clients: Vec<(Client, Position)> = vec![];
log::debug!("{config_toml:?}");
let release_bind = config_toml
.as_ref()
.and_then(|c| c.release_bind.clone())
.unwrap_or(Vec::from_iter(DEFAULT_RELEASE_KEYS.iter().cloned()));
let mut clients: Vec<(TomlClient, Position)> = vec![];
if let Some(config_toml) = config_toml {
if let Some(c) = config_toml.right {
@@ -133,43 +168,40 @@ impl Config {
}
}
Ok(Config { frontend, clients, port })
let daemon = args.daemon;
Ok(Config {
daemon,
frontend,
clients,
port,
release_bind,
})
}
pub fn get_clients(&self) -> Vec<(HashSet<SocketAddr>, Option<String>, Position)> {
self.clients.iter().map(|(c,p)| {
let port = c.port.unwrap_or(DEFAULT_PORT);
// add ips from config
let config_ips: Vec<IpAddr> = if let Some(ips) = c.ips.as_ref() {
ips.iter().cloned().collect()
} else {
vec![]
};
let host_name = c.host_name.clone();
// add ips from dns lookup
let dns_ips = match host_name.as_ref() {
None => vec![],
Some(host_name) => match dns::resolve(host_name) {
Err(e) => {
log::warn!("{host_name}: could not resolve host: {e}");
vec![]
}
Ok(l) if l.is_empty() => {
log::warn!("{host_name}: could not resolve host");
vec![]
}
Ok(l) => l,
pub fn get_clients(&self) -> Vec<ConfigClient> {
self.clients
.iter()
.map(|(c, pos)| {
let port = c.port.unwrap_or(DEFAULT_PORT);
let ips: HashSet<IpAddr> = if let Some(ips) = c.ips.as_ref() {
HashSet::from_iter(ips.iter().cloned())
} else {
HashSet::new()
};
let hostname = match &c.hostname {
Some(h) => Some(h.clone()),
None => c.host_name.clone(),
};
let active = c.activate_on_startup.unwrap_or(false);
ConfigClient {
ips,
hostname,
port,
pos: *pos,
active,
}
};
if config_ips.is_empty() && dns_ips.is_empty() {
log::error!("no ips found for client {p:?}, ignoring!");
log::error!("You can manually specify ip addresses via the `ips` config option");
}
let ips = config_ips.into_iter().chain(dns_ips.into_iter());
// map ip addresses to socket addresses
let addrs: HashSet<SocketAddr> = ips.map(|ip| SocketAddr::new(ip, port)).collect();
(addrs, host_name, *p)
}).filter(|(a, _, _)| !a.is_empty()).collect()
})
.collect()
}
}

View File

@@ -1,99 +0,0 @@
#[cfg(unix)]
use std::env;
use anyhow::Result;
use crate::{backend::consumer, client::{ClientHandle, ClientEvent}, event::Event};
#[cfg(unix)]
#[derive(Debug)]
enum Backend {
Wlroots,
X11,
RemoteDesktopPortal,
Libei,
}
pub trait EventConsumer {
/// Event corresponding to an abstract `client_handle`
fn consume(&self, event: Event, client_handle: ClientHandle);
/// Event corresponding to a configuration change
fn notify(&mut self, client_event: ClientEvent);
}
pub fn create() -> Result<Box<dyn EventConsumer>> {
#[cfg(windows)]
return Ok(Box::new(consumer::windows::WindowsConsumer::new()));
#[cfg(unix)]
let backend = match env::var("XDG_SESSION_TYPE") {
Ok(session_type) => match session_type.as_str() {
"x11" => {
log::info!("XDG_SESSION_TYPE = x11 -> using x11 event consumer");
Backend::X11
}
"wayland" => {
log::info!("XDG_SESSION_TYPE = wayland -> using wayland event consumer");
match env::var("XDG_CURRENT_DESKTOP") {
Ok(current_desktop) => match current_desktop.as_str() {
"gnome" => {
log::info!("XDG_CURRENT_DESKTOP = gnome -> using libei backend");
Backend::Libei
}
"KDE" => {
log::info!("XDG_CURRENT_DESKTOP = KDE -> using xdg_desktop_portal backend");
Backend::RemoteDesktopPortal
}
"sway" => {
log::info!("XDG_CURRENT_DESKTOP = sway -> using wlroots backend");
Backend::Wlroots
}
"Hyprland" => {
log::info!("XDG_CURRENT_DESKTOP = Hyprland -> using wlroots backend");
Backend::Wlroots
}
_ => {
log::warn!("unknown XDG_CURRENT_DESKTOP -> defaulting to wlroots backend");
Backend::Wlroots
}
}
// default to wlroots backend for now
_ => {
log::warn!("unknown XDG_CURRENT_DESKTOP -> defaulting to wlroots backend");
Backend::Wlroots
}
}
}
_ => panic!("unknown XDG_SESSION_TYPE"),
},
Err(_) => panic!("could not detect session type: XDG_SESSION_TYPE environment variable not set!"),
};
#[cfg(unix)]
match backend {
Backend::Libei => {
#[cfg(not(feature = "libei"))]
panic!("feature libei not enabled");
#[cfg(feature = "libei")]
Ok(Box::new(consumer::libei::LibeiConsumer::new()))
},
Backend::RemoteDesktopPortal => {
#[cfg(not(feature = "xdg_desktop_portal"))]
panic!("feature xdg_desktop_portal not enabled");
#[cfg(feature = "xdg_desktop_portal")]
Ok(Box::new(consumer::xdg_desktop_portal::DesktopPortalConsumer::new()))
},
Backend::Wlroots => {
#[cfg(not(feature = "wayland"))]
panic!("feature wayland not enabled");
#[cfg(feature = "wayland")]
Ok(Box::new(consumer::wlroots::WlrootsConsumer::new()?))
},
Backend::X11 => {
#[cfg(not(feature = "x11"))]
panic!("feature x11 not enabled");
#[cfg(feature = "x11")]
Ok(Box::new(consumer::x11::X11Consumer::new()))
},
}
}

View File

@@ -1,9 +1,23 @@
use anyhow::Result;
use std::{error::Error, net::IpAddr};
use trust_dns_resolver::Resolver;
use trust_dns_resolver::TokioAsyncResolver;
pub fn resolve(host: &str) -> Result<Vec<IpAddr>, Box<dyn Error>> {
log::info!("resolving {host} ...");
let response = Resolver::from_system_conf()?.lookup_ip(host)?;
Ok(response.iter().collect())
pub struct DnsResolver {
resolver: TokioAsyncResolver,
}
impl DnsResolver {
pub(crate) async fn new() -> Result<Self> {
let resolver = TokioAsyncResolver::tokio_from_system_conf()?;
Ok(Self { resolver })
}
pub(crate) async fn resolve(&self, host: &str) -> Result<Vec<IpAddr>, Box<dyn Error>> {
log::info!("resolving {host} ...");
let response = self.resolver.lookup_ip(host).await?;
for ip in response.iter() {
log::info!("{host}: adding ip {ip}");
}
Ok(response.iter().collect())
}
}

98
src/emulate.rs Normal file
View File

@@ -0,0 +1,98 @@
use async_trait::async_trait;
use std::future;
use crate::{
client::{ClientEvent, ClientHandle},
event::Event,
};
use anyhow::Result;
#[cfg(windows)]
pub mod windows;
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
pub mod x11;
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
pub mod wlroots;
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
pub mod xdg_desktop_portal;
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
pub mod libei;
#[cfg(target_os = "macos")]
pub mod macos;
/// fallback input emulation (logs events)
pub mod dummy;
#[async_trait]
pub trait InputEmulation: Send {
async fn consume(&mut self, event: Event, client_handle: ClientHandle);
async fn notify(&mut self, client_event: ClientEvent);
/// this function is waited on continuously and can be used to handle events
async fn dispatch(&mut self) -> Result<()> {
let _: () = future::pending().await;
Ok(())
}
async fn destroy(&mut self);
}
pub async fn create() -> Box<dyn InputEmulation> {
#[cfg(windows)]
match windows::WindowsEmulation::new() {
Ok(c) => return Box::new(c),
Err(e) => log::warn!("windows input emulation unavailable: {e}"),
}
#[cfg(target_os = "macos")]
match macos::MacOSEmulation::new() {
Ok(c) => {
log::info!("using macos input emulation");
return Box::new(c);
}
Err(e) => log::error!("macos input emulatino not available: {e}"),
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
match wlroots::WlrootsEmulation::new() {
Ok(c) => {
log::info!("using wlroots input emulation");
return Box::new(c);
}
Err(e) => log::info!("wayland backend not available: {e}"),
}
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
match libei::LibeiEmulation::new().await {
Ok(c) => {
log::info!("using libei input emulation");
return Box::new(c);
}
Err(e) => log::info!("libei not available: {e}"),
}
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
match xdg_desktop_portal::DesktopPortalEmulation::new().await {
Ok(c) => {
log::info!("using xdg-remote-desktop-portal input emulation");
return Box::new(c);
}
Err(e) => log::info!("remote desktop portal not available: {e}"),
}
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
match x11::X11Emulation::new() {
Ok(c) => {
log::info!("using x11 input emulation");
return Box::new(c);
}
Err(e) => log::info!("x11 input emulation not available: {e}"),
}
log::error!("falling back to dummy input emulation");
Box::new(dummy::DummyEmulation::new())
}

26
src/emulate/dummy.rs Normal file
View File

@@ -0,0 +1,26 @@
use crate::{
client::{ClientEvent, ClientHandle},
emulate::InputEmulation,
event::Event,
};
use async_trait::async_trait;
#[derive(Default)]
pub struct DummyEmulation;
impl DummyEmulation {
pub fn new() -> Self {
Self {}
}
}
#[async_trait]
impl InputEmulation for DummyEmulation {
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) {}
}

387
src/emulate/libei.rs Normal file
View File

@@ -0,0 +1,387 @@
use std::{
collections::HashMap,
os::{fd::OwnedFd, unix::net::UnixStream},
time::{SystemTime, UNIX_EPOCH},
};
use anyhow::{anyhow, Result};
use ashpd::{
desktop::{
remote_desktop::{DeviceType, RemoteDesktop},
ResponseError,
},
WindowIdentifier,
};
use async_trait::async_trait;
use futures::StreamExt;
use reis::{
ei::{self, button::ButtonState, handshake::ContextType, keyboard::KeyState},
tokio::EiEventStream,
PendingRequestResult,
};
use crate::{
client::{ClientEvent, ClientHandle},
emulate::InputEmulation,
event::Event,
};
pub struct LibeiEmulation {
handshake: bool,
context: ei::Context,
events: EiEventStream,
pointer: Option<(ei::Device, ei::Pointer)>,
has_pointer: bool,
scroll: Option<(ei::Device, ei::Scroll)>,
has_scroll: bool,
button: Option<(ei::Device, ei::Button)>,
has_button: bool,
keyboard: Option<(ei::Device, ei::Keyboard)>,
has_keyboard: bool,
capabilities: HashMap<String, u64>,
capability_mask: u64,
sequence: u32,
serial: u32,
}
async fn get_ei_fd() -> Result<OwnedFd, ashpd::Error> {
let proxy = RemoteDesktop::new().await?;
// retry when user presses the cancel button
let (session, _) = loop {
log::debug!("creating session ...");
let session = proxy.create_session().await?;
log::debug!("selecting devices ...");
proxy
.select_devices(&session, DeviceType::Keyboard | DeviceType::Pointer)
.await?;
log::info!("requesting permission for input emulation");
match proxy
.start(&session, &WindowIdentifier::default())
.await?
.response()
{
Ok(d) => break (session, d),
Err(ashpd::Error::Response(ResponseError::Cancelled)) => {
log::warn!("request cancelled!");
continue;
}
e => e?,
};
};
proxy.connect_to_eis(&session).await
}
impl LibeiEmulation {
pub async fn new() -> Result<Self> {
// fd is owned by the message, so we need to dup it
let eifd = get_ei_fd().await?;
let stream = UnixStream::from(eifd);
// let stream = UnixStream::connect("/run/user/1000/eis-0")?;
stream.set_nonblocking(true)?;
let context = ei::Context::new(stream)?;
context.flush()?;
let events = EiEventStream::new(context.clone())?;
Ok(Self {
handshake: false,
context,
events,
pointer: None,
button: None,
scroll: None,
keyboard: None,
has_pointer: false,
has_button: false,
has_scroll: false,
has_keyboard: false,
capabilities: HashMap::new(),
capability_mask: 0,
sequence: 0,
serial: 0,
})
}
}
#[async_trait]
impl InputEmulation for LibeiEmulation {
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) {}
}

279
src/emulate/macos.rs Normal file
View File

@@ -0,0 +1,279 @@
use crate::client::{ClientEvent, ClientHandle};
use crate::emulate::InputEmulation;
use crate::event::{Event, KeyboardEvent, PointerEvent};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use core_graphics::display::{CGDisplayBounds, CGMainDisplayID, CGPoint};
use core_graphics::event::{
CGEvent, CGEventTapLocation, CGEventType, CGKeyCode, CGMouseButton, EventField, ScrollEventUnit,
};
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
use keycode::{KeyMap, KeyMapping};
use std::ops::{Index, IndexMut};
use std::time::Duration;
use tokio::task::AbortHandle;
const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500);
const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32);
pub struct MacOSEmulation {
pub event_source: CGEventSource,
repeat_task: Option<AbortHandle>,
button_state: ButtonState,
}
struct ButtonState {
left: bool,
right: bool,
center: bool,
}
impl Index<CGMouseButton> for ButtonState {
type Output = bool;
fn index(&self, index: CGMouseButton) -> &Self::Output {
match index {
CGMouseButton::Left => &self.left,
CGMouseButton::Right => &self.right,
CGMouseButton::Center => &self.center,
}
}
}
impl IndexMut<CGMouseButton> for ButtonState {
fn index_mut(&mut self, index: CGMouseButton) -> &mut Self::Output {
match index {
CGMouseButton::Left => &mut self.left,
CGMouseButton::Right => &mut self.right,
CGMouseButton::Center => &mut self.center,
}
}
}
unsafe impl Send for MacOSEmulation {}
impl MacOSEmulation {
pub fn new() -> Result<Self> {
let event_source = match CGEventSource::new(CGEventSourceStateID::CombinedSessionState) {
Ok(e) => e,
Err(_) => return Err(anyhow!("event source creation failed!")),
};
let button_state = ButtonState {
left: false,
right: false,
center: false,
};
Ok(Self {
event_source,
button_state,
repeat_task: None,
})
}
fn get_mouse_location(&self) -> Option<CGPoint> {
let event: CGEvent = CGEvent::new(self.event_source.clone()).ok()?;
Some(event.location())
}
async fn spawn_repeat_task(&mut self, key: u16) {
// there can only be one repeating key and it's
// always the last to be pressed
self.kill_repeat_task();
let event_source = self.event_source.clone();
let repeat_task = tokio::task::spawn_local(async move {
tokio::time::sleep(DEFAULT_REPEAT_DELAY).await;
loop {
key_event(event_source.clone(), key, 1);
tokio::time::sleep(DEFAULT_REPEAT_INTERVAL).await;
}
});
self.repeat_task = Some(repeat_task.abort_handle());
}
fn kill_repeat_task(&mut self) {
if let Some(task) = self.repeat_task.take() {
task.abort();
}
}
}
fn key_event(event_source: CGEventSource, key: u16, state: u8) {
let event = match CGEvent::new_keyboard_event(event_source, key, state != 0) {
Ok(e) => e,
Err(_) => {
log::warn!("unable to create key event");
return;
}
};
event.post(CGEventTapLocation::HID);
}
#[async_trait]
impl InputEmulation for MacOSEmulation {
async fn consume(&mut self, event: Event, _client_handle: ClientHandle) {
match event {
Event::Pointer(pointer_event) => match pointer_event {
PointerEvent::Motion {
time: _,
relative_x,
relative_y,
} => {
// FIXME secondary displays?
let (min_x, min_y, max_x, max_y) = unsafe {
let display = CGMainDisplayID();
let bounds = CGDisplayBounds(display);
let min_x = bounds.origin.x;
let max_x = bounds.origin.x + bounds.size.width;
let min_y = bounds.origin.y;
let max_y = bounds.origin.y + bounds.size.height;
(min_x as f64, min_y as f64, max_x as f64, max_y as f64)
};
let mut mouse_location = match self.get_mouse_location() {
Some(l) => l,
None => {
log::warn!("could not get mouse location!");
return;
}
};
mouse_location.x = (mouse_location.x + relative_x).clamp(min_x, max_x - 1.);
mouse_location.y = (mouse_location.y + relative_y).clamp(min_y, max_y - 1.);
let mut event_type = CGEventType::MouseMoved;
if self.button_state.left {
event_type = CGEventType::LeftMouseDragged
} else if self.button_state.right {
event_type = CGEventType::RightMouseDragged
} else if self.button_state.center {
event_type = CGEventType::OtherMouseDragged
};
let event = match CGEvent::new_mouse_event(
self.event_source.clone(),
event_type,
mouse_location,
CGMouseButton::Left,
) {
Ok(e) => e,
Err(_) => {
log::warn!("mouse event creation failed!");
return;
}
};
event.set_integer_value_field(
EventField::MOUSE_EVENT_DELTA_X,
relative_x as i64,
);
event.set_integer_value_field(
EventField::MOUSE_EVENT_DELTA_Y,
relative_y as i64,
);
event.post(CGEventTapLocation::HID);
}
PointerEvent::Button {
time: _,
button,
state,
} => {
let (event_type, mouse_button) = match (button, state) {
(b, 1) if b == crate::event::BTN_LEFT => {
(CGEventType::LeftMouseDown, CGMouseButton::Left)
}
(b, 0) if b == crate::event::BTN_LEFT => {
(CGEventType::LeftMouseUp, CGMouseButton::Left)
}
(b, 1) if b == crate::event::BTN_RIGHT => {
(CGEventType::RightMouseDown, CGMouseButton::Right)
}
(b, 0) if b == crate::event::BTN_RIGHT => {
(CGEventType::RightMouseUp, CGMouseButton::Right)
}
(b, 1) if b == crate::event::BTN_MIDDLE => {
(CGEventType::OtherMouseDown, CGMouseButton::Center)
}
(b, 0) if b == crate::event::BTN_MIDDLE => {
(CGEventType::OtherMouseUp, CGMouseButton::Center)
}
_ => {
log::warn!("invalid button event: {button},{state}");
return;
}
};
// store button state
self.button_state[mouse_button] = state == 1;
let location = self.get_mouse_location().unwrap();
let event = match CGEvent::new_mouse_event(
self.event_source.clone(),
event_type,
location,
mouse_button,
) {
Ok(e) => e,
Err(()) => {
log::warn!("mouse event creation failed!");
return;
}
};
event.post(CGEventTapLocation::HID);
}
PointerEvent::Axis {
time: _,
axis,
value,
} => {
let value = value as i32 / 10; // FIXME: high precision scroll events
let (count, wheel1, wheel2, wheel3) = match axis {
0 => (1, value, 0, 0), // 0 = vertical => 1 scroll wheel device (y axis)
1 => (2, 0, value, 0), // 1 = horizontal => 2 scroll wheel devices (y, x) -> (0, x)
_ => {
log::warn!("invalid scroll event: {axis}, {value}");
return;
}
};
let event = match CGEvent::new_scroll_event(
self.event_source.clone(),
ScrollEventUnit::LINE,
count,
wheel1,
wheel2,
wheel3,
) {
Ok(e) => e,
Err(()) => {
log::warn!("scroll event creation failed!");
return;
}
};
event.post(CGEventTapLocation::HID);
}
PointerEvent::Frame { .. } => {}
},
Event::Keyboard(keyboard_event) => match keyboard_event {
KeyboardEvent::Key {
time: _,
key,
state,
} => {
let code = match KeyMap::from_key_mapping(KeyMapping::Evdev(key as u16)) {
Ok(k) => k.mac as CGKeyCode,
Err(_) => {
log::warn!("unable to map key event");
return;
}
};
match state {
// pressed
1 => self.spawn_repeat_task(code).await,
_ => self.kill_repeat_task(),
}
key_event(self.event_source.clone(), code, state)
}
KeyboardEvent::Modifiers { .. } => {}
},
_ => (),
}
}
async fn notify(&mut self, _client_event: ClientEvent) {}
async fn destroy(&mut self) {}
}

231
src/emulate/windows.rs Normal file
View File

@@ -0,0 +1,231 @@
use crate::{
emulate::InputEmulation,
event::{KeyboardEvent, PointerEvent},
scancode,
};
use anyhow::Result;
use async_trait::async_trait;
use std::ops::BitOrAssign;
use std::time::Duration;
use tokio::task::AbortHandle;
use windows::Win32::UI::Input::KeyboardAndMouse::{SendInput, INPUT_0, KEYEVENTF_EXTENDEDKEY};
use windows::Win32::UI::Input::KeyboardAndMouse::{
INPUT, INPUT_KEYBOARD, INPUT_MOUSE, KEYBDINPUT, KEYEVENTF_KEYUP, KEYEVENTF_SCANCODE,
MOUSEEVENTF_HWHEEL, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MIDDLEDOWN,
MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP,
MOUSEEVENTF_WHEEL, MOUSEINPUT,
};
use crate::{
client::{ClientEvent, ClientHandle},
event::Event,
};
const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500);
const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32);
pub struct WindowsEmulation {
repeat_task: Option<AbortHandle>,
}
impl WindowsEmulation {
pub fn new() -> Result<Self> {
Ok(Self { repeat_task: None })
}
}
#[async_trait]
impl InputEmulation for WindowsEmulation {
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,
} => {
match state {
// pressed
0 => self.kill_repeat_task(),
1 => self.spawn_repeat_task(key).await,
_ => {}
}
key_event(key, state)
}
KeyboardEvent::Modifiers { .. } => {}
},
_ => {}
}
}
async fn notify(&mut self, _: ClientEvent) {
// nothing to do
}
async fn destroy(&mut self) {}
}
impl WindowsEmulation {
async fn spawn_repeat_task(&mut self, key: u32) {
// there can only be one repeating key and it's
// always the last to be pressed
self.kill_repeat_task();
let repeat_task = tokio::task::spawn_local(async move {
tokio::time::sleep(DEFAULT_REPEAT_DELAY).await;
loop {
key_event(key, 1);
tokio::time::sleep(DEFAULT_REPEAT_INTERVAL).await;
}
});
self.repeat_task = Some(repeat_task.abort_handle());
}
fn kill_repeat_task(&mut self) {
if let Some(task) = self.repeat_task.take() {
task.abort();
}
}
}
fn send_input_safe(input: INPUT) {
unsafe {
loop {
/* retval = number of successfully submitted events */
if SendInput(&[input], std::mem::size_of::<INPUT>() as i32) > 0 {
break;
}
}
}
}
fn send_mouse_input(mi: MOUSEINPUT) {
send_input_safe(INPUT {
r#type: INPUT_MOUSE,
Anonymous: INPUT_0 { mi },
});
}
fn send_keyboard_input(ki: KEYBDINPUT) {
send_input_safe(INPUT {
r#type: INPUT_KEYBOARD,
Anonymous: INPUT_0 { ki },
});
}
fn rel_mouse(dx: i32, dy: i32) {
let mi = MOUSEINPUT {
dx,
dy,
mouseData: 0,
dwFlags: MOUSEEVENTF_MOVE,
time: 0,
dwExtraInfo: 0,
};
send_mouse_input(mi);
}
fn mouse_button(button: u32, state: u32) {
let dw_flags = match state {
0 => match button {
0x110 => MOUSEEVENTF_LEFTUP,
0x111 => MOUSEEVENTF_RIGHTUP,
0x112 => MOUSEEVENTF_MIDDLEUP,
_ => return,
},
1 => match button {
0x110 => MOUSEEVENTF_LEFTDOWN,
0x111 => MOUSEEVENTF_RIGHTDOWN,
0x112 => MOUSEEVENTF_MIDDLEDOWN,
_ => return,
},
_ => return,
};
let mi = MOUSEINPUT {
dx: 0,
dy: 0, // no movement
mouseData: 0,
dwFlags: dw_flags,
time: 0,
dwExtraInfo: 0,
};
send_mouse_input(mi);
}
fn scroll(axis: u8, value: f64) {
let event_type = match axis {
0 => MOUSEEVENTF_WHEEL,
1 => MOUSEEVENTF_HWHEEL,
_ => return,
};
let mi = MOUSEINPUT {
dx: 0,
dy: 0,
mouseData: (-value * 15.0) as i32 as u32,
dwFlags: event_type,
time: 0,
dwExtraInfo: 0,
};
send_mouse_input(mi);
}
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 mut flags = KEYEVENTF_SCANCODE;
if extended {
flags.bitor_assign(KEYEVENTF_EXTENDEDKEY);
}
if state == 0 {
flags.bitor_assign(KEYEVENTF_KEYUP);
}
let ki = KEYBDINPUT {
wVk: Default::default(),
wScan: scancode,
dwFlags: flags,
time: 0,
dwExtraInfo: 0,
};
send_keyboard_input(ki);
}
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)
}

257
src/emulate/wlroots.rs Normal file
View File

@@ -0,0 +1,257 @@
use crate::client::{ClientEvent, ClientHandle};
use crate::emulate::InputEmulation;
use async_trait::async_trait;
use std::collections::HashMap;
use std::io;
use std::os::fd::{AsFd, OwnedFd};
use wayland_client::backend::WaylandError;
use wayland_client::WEnum;
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,
};
use wayland_protocols_misc::zwp_virtual_keyboard_v1::client::{
zwp_virtual_keyboard_manager_v1::ZwpVirtualKeyboardManagerV1 as VkManager,
zwp_virtual_keyboard_v1::ZwpVirtualKeyboardV1 as Vk,
};
use wayland_client::{
delegate_noop,
globals::{registry_queue_init, GlobalListContents},
protocol::{wl_registry, wl_seat},
Connection, Dispatch, EventQueue, QueueHandle,
};
use crate::event::{Event, KeyboardEvent, PointerEvent};
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
pub(crate) struct WlrootsEmulation {
last_flush_failed: bool,
state: State,
queue: EventQueue<State>,
}
impl WlrootsEmulation {
pub fn new() -> Result<Self> {
let conn = Connection::connect_to_env()?;
let (globals, queue) = registry_queue_init::<State>(&conn)?;
let qh = queue.handle();
let seat: wl_seat::WlSeat = match globals.bind(&qh, 7..=8, ()) {
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 mut emulate = WlrootsEmulation {
last_flush_failed: false,
state: State {
keymap: None,
input_for_client,
seat,
vpm,
vkm,
qh,
},
queue,
};
while emulate.state.keymap.is_none() {
emulate.queue.blocking_dispatch(&mut emulate.state).unwrap();
}
// let fd = unsafe { &File::from_raw_fd(emulate.state.keymap.unwrap().1.as_raw_fd()) };
// let mmap = unsafe { MmapOptions::new().map_copy(fd).unwrap() };
// log::debug!("{:?}", &mmap[..100]);
Ok(emulate)
}
}
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 InputEmulation for WlrootsEmulation {
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<(), ()> {
match event {
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,
} => {
let state: ButtonState = state.try_into()?;
self.pointer.button(time, button, state);
}
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(),
}
self.pointer.frame();
}
Event::Keyboard(e) => match e {
KeyboardEvent::Key { time, key, state } => {
self.keyboard.key(time, key, state as u32);
}
KeyboardEvent::Modifiers {
mods_depressed,
mods_latched,
mods_locked,
group,
} => {
self.keyboard
.modifiers(mods_depressed, mods_latched, mods_locked, group);
}
},
_ => {}
}
Ok(())
}
}
delegate_noop!(State: Vp);
delegate_noop!(State: Vk);
delegate_noop!(State: VpManager);
delegate_noop!(State: VkManager);
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for State {
fn event(
_: &mut State,
_: &wl_registry::WlRegistry,
_: wl_registry::Event,
_: &GlobalListContents,
_: &Connection,
_: &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, ());
}
}
}
}

149
src/emulate/x11.rs Normal file
View File

@@ -0,0 +1,149 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use std::ptr;
use x11::{
xlib::{self, XCloseDisplay},
xtest,
};
use crate::{
client::ClientHandle,
emulate::InputEmulation,
event::{
Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT,
},
};
pub struct X11Emulation {
display: *mut xlib::Display,
}
unsafe impl Send for X11Emulation {}
impl X11Emulation {
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);
}
}
}
impl Drop for X11Emulation {
fn drop(&mut self) {
unsafe {
XCloseDisplay(self.display);
}
}
}
#[async_trait]
impl InputEmulation for X11Emulation {
async fn consume(&mut self, event: Event, _: ClientHandle) {
match event {
Event::Pointer(pointer_event) => match pointer_event {
PointerEvent::Motion {
time: _,
relative_x,
relative_y,
} => {
self.relative_motion(relative_x as i32, relative_y as i32);
}
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(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

@@ -0,0 +1,154 @@
use anyhow::Result;
use ashpd::{
desktop::{
remote_desktop::{Axis, DeviceType, KeyState, RemoteDesktop},
ResponseError, Session,
},
WindowIdentifier,
};
use async_trait::async_trait;
use crate::{
client::ClientEvent,
emulate::InputEmulation,
event::{
Event::{Keyboard, Pointer},
KeyboardEvent, PointerEvent,
},
};
pub struct DesktopPortalEmulation<'a> {
proxy: RemoteDesktop<'a>,
session: Session<'a>,
}
impl<'a> DesktopPortalEmulation<'a> {
pub async fn new() -> Result<DesktopPortalEmulation<'a>> {
log::debug!("connecting to org.freedesktop.portal.RemoteDesktop portal ...");
let proxy = RemoteDesktop::new().await?;
// retry when user presses the cancel button
let (session, _) = loop {
log::debug!("creating session ...");
let session = proxy.create_session().await?;
log::debug!("selecting devices ...");
proxy
.select_devices(&session, DeviceType::Keyboard | DeviceType::Pointer)
.await?;
log::info!("requesting permission for input emulation");
match proxy
.start(&session, &WindowIdentifier::default())
.await?
.response()
{
Ok(d) => break (session, d),
Err(ashpd::Error::Response(ResponseError::Cancelled)) => {
log::warn!("request cancelled!");
continue;
}
e => e?,
};
};
log::debug!("started session");
Ok(Self { proxy, session })
}
}
#[async_trait]
impl<'a> InputEmulation for DesktopPortalEmulation<'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,8 +1,17 @@
use std::{error::Error, fmt::{self, Display}};
use anyhow::{anyhow, Result};
use std::{
error::Error,
fmt::{self, Display},
};
pub mod server;
// FIXME
pub const BTN_LEFT: u32 = 0x110;
pub const BTN_RIGHT: u32 = 0x111;
pub const BTN_MIDDLE: u32 = 0x112;
pub const BTN_BACK: u32 = 0x113;
pub const BTN_FORWARD: u32 = 0x114;
#[derive(Debug, Clone, Copy)]
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum PointerEvent {
Motion {
time: u32,
@@ -22,7 +31,7 @@ pub enum PointerEvent {
Frame {},
}
#[derive(Debug, Clone, Copy)]
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum KeyboardEvent {
Key {
time: u32,
@@ -37,22 +46,50 @@ pub enum KeyboardEvent {
},
}
#[derive(Debug, Clone, Copy)]
#[derive(PartialEq, Debug, Clone, Copy)]
pub enum Event {
/// pointer event (motion / button / axis)
Pointer(PointerEvent),
/// keyboard events (key / modifiers)
Keyboard(KeyboardEvent),
Release(),
/// enter event: request to enter a client.
/// The client must release the pointer if it is grabbed
/// and reply with a leave event, as soon as its ready to
/// receive events
Enter(),
/// leave event: this client is now ready to receive events and will
/// not send any events after until it sends an enter event
Leave(),
/// ping a client, to see if it is still alive. A client that does
/// not respond with a pong event will be assumed to be offline.
Ping(),
/// response to a ping event: this event signals that a client
/// is still alive but must otherwise be ignored
Pong(),
/// explicit disconnect request. The client will no longer
/// send events until the next Enter event. All of its keys should be released.
Disconnect(),
}
impl Display for PointerEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PointerEvent::Motion { time: _ , relative_x, relative_y } => write!(f, "motion({relative_x},{relative_y})"),
PointerEvent::Button { time: _ , button, state } => write!(f, "button({button}, {state})"),
PointerEvent::Axis { time: _, axis, value } => write!(f, "scroll({axis}, {value})"),
PointerEvent::Frame { } => write!(f, "frame()"),
PointerEvent::Motion {
time: _,
relative_x,
relative_y,
} => write!(f, "motion({relative_x},{relative_y})"),
PointerEvent::Button {
time: _,
button,
state,
} => write!(f, "button({button}, {state})"),
PointerEvent::Axis {
time: _,
axis,
value,
} => write!(f, "scroll({axis}, {value})"),
PointerEvent::Frame {} => write!(f, "frame()"),
}
}
}
@@ -60,8 +97,20 @@ impl Display for PointerEvent {
impl Display for KeyboardEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
KeyboardEvent::Key { time: _, key, state } => write!(f, "key({key}, {state})"),
KeyboardEvent::Modifiers { mods_depressed, mods_latched, mods_locked, group } => write!(f, "modifiers({mods_depressed},{mods_latched},{mods_locked},{group})"),
KeyboardEvent::Key {
time: _,
key,
state,
} => write!(f, "key({key}, {state})"),
KeyboardEvent::Modifiers {
mods_depressed,
mods_latched,
mods_locked,
group,
} => write!(
f,
"modifiers({mods_depressed},{mods_latched},{mods_locked},{group})"
),
}
}
}
@@ -71,9 +120,11 @@ impl Display for Event {
match self {
Event::Pointer(p) => write!(f, "{}", p),
Event::Keyboard(k) => write!(f, "{}", k),
Event::Release() => write!(f, "release"),
Event::Enter() => write!(f, "enter"),
Event::Leave() => write!(f, "leave"),
Event::Ping() => write!(f, "ping"),
Event::Pong() => write!(f, "pong"),
Event::Disconnect() => write!(f, "disconnect"),
}
}
}
@@ -81,11 +132,13 @@ impl Display for Event {
impl Event {
fn event_type(&self) -> EventType {
match self {
Self::Pointer(_) => EventType::POINTER,
Self::Keyboard(_) => EventType::KEYBOARD,
Self::Release() => EventType::RELEASE,
Self::Ping() => EventType::PING,
Self::Pong() => EventType::PONG,
Self::Pointer(_) => EventType::Pointer,
Self::Keyboard(_) => EventType::Keyboard,
Self::Enter() => EventType::Enter,
Self::Leave() => EventType::Leave,
Self::Ping() => EventType::Ping,
Self::Pong() => EventType::Pong,
Self::Disconnect() => EventType::Disconnect,
}
}
}
@@ -93,10 +146,10 @@ impl Event {
impl PointerEvent {
fn event_type(&self) -> PointerEventType {
match self {
Self::Motion { .. } => PointerEventType::MOTION,
Self::Button { .. } => PointerEventType::BUTTON,
Self::Axis { .. } => PointerEventType::AXIS,
Self::Frame { .. } => PointerEventType::FRAME,
Self::Motion { .. } => PointerEventType::Motion,
Self::Button { .. } => PointerEventType::Button,
Self::Axis { .. } => PointerEventType::Axis,
Self::Frame { .. } => PointerEventType::Frame,
}
}
}
@@ -104,40 +157,42 @@ impl PointerEvent {
impl KeyboardEvent {
fn event_type(&self) -> KeyboardEventType {
match self {
KeyboardEvent::Key { .. } => KeyboardEventType::KEY,
KeyboardEvent::Modifiers { .. } => KeyboardEventType::MODIFIERS,
KeyboardEvent::Key { .. } => KeyboardEventType::Key,
KeyboardEvent::Modifiers { .. } => KeyboardEventType::Modifiers,
}
}
}
enum PointerEventType {
MOTION,
BUTTON,
AXIS,
FRAME,
Motion,
Button,
Axis,
Frame,
}
enum KeyboardEventType {
KEY,
MODIFIERS,
Key,
Modifiers,
}
enum EventType {
POINTER,
KEYBOARD,
RELEASE,
PING,
PONG,
Pointer,
Keyboard,
Enter,
Leave,
Ping,
Pong,
Disconnect,
}
impl TryFrom<u8> for PointerEventType {
type Error = Box<dyn Error>;
type Error = anyhow::Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
fn try_from(value: u8) -> Result<Self> {
match value {
x if x == Self::MOTION as u8 => Ok(Self::MOTION),
x if x == Self::BUTTON as u8 => Ok(Self::BUTTON),
x if x == Self::AXIS as u8 => Ok(Self::AXIS),
x if x == Self::FRAME as u8 => Ok(Self::FRAME),
_ => Err(Box::new(ProtocolError {
x if x == Self::Motion as u8 => Ok(Self::Motion),
x if x == Self::Button as u8 => Ok(Self::Button),
x if x == Self::Axis as u8 => Ok(Self::Axis),
x if x == Self::Frame as u8 => Ok(Self::Frame),
_ => Err(anyhow!(ProtocolError {
msg: format!("invalid pointer event type {}", value),
})),
}
@@ -145,30 +200,32 @@ impl TryFrom<u8> for PointerEventType {
}
impl TryFrom<u8> for KeyboardEventType {
type Error = Box<dyn Error>;
type Error = anyhow::Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
fn try_from(value: u8) -> Result<Self> {
match value {
x if x == Self::KEY as u8 => Ok(Self::KEY),
x if x == Self::MODIFIERS as u8 => Ok(Self::MODIFIERS),
_ => Err(Box::new(ProtocolError {
x if x == Self::Key as u8 => Ok(Self::Key),
x if x == Self::Modifiers as u8 => Ok(Self::Modifiers),
_ => Err(anyhow!(ProtocolError {
msg: format!("invalid keyboard event type {}", value),
})),
}
}
}
impl Into<Vec<u8>> for &Event {
fn into(self) -> Vec<u8> {
let event_id = vec![self.event_type() as u8];
let event_data = match self {
impl From<&Event> for Vec<u8> {
fn from(event: &Event) -> Self {
let event_id = vec![event.event_type() as u8];
let event_data = match event {
Event::Pointer(p) => p.into(),
Event::Keyboard(k) => k.into(),
Event::Release() => vec![],
Event::Enter() => vec![],
Event::Leave() => vec![],
Event::Ping() => vec![],
Event::Pong() => vec![],
Event::Disconnect() => vec![],
};
vec![event_id, event_data].concat()
[event_id, event_data].concat()
}
}
@@ -185,27 +242,29 @@ impl fmt::Display for ProtocolError {
impl Error for ProtocolError {}
impl TryFrom<Vec<u8>> for Event {
type Error = Box<dyn Error>;
type Error = anyhow::Error;
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
fn try_from(value: Vec<u8>) -> Result<Self> {
let event_id = u8::from_be_bytes(value[..1].try_into()?);
match event_id {
i if i == (EventType::POINTER as u8) => Ok(Event::Pointer(value.try_into()?)),
i if i == (EventType::KEYBOARD as u8) => Ok(Event::Keyboard(value.try_into()?)),
i if i == (EventType::RELEASE as u8) => Ok(Event::Release()),
i if i == (EventType::PING as u8) => Ok(Event::Ping()),
i if i == (EventType::PONG as u8) => Ok(Event::Pong()),
_ => Err(Box::new(ProtocolError {
i if i == (EventType::Pointer as u8) => Ok(Event::Pointer(value.try_into()?)),
i if i == (EventType::Keyboard as u8) => Ok(Event::Keyboard(value.try_into()?)),
i if i == (EventType::Enter as u8) => Ok(Event::Enter()),
i if i == (EventType::Leave as u8) => Ok(Event::Leave()),
i if i == (EventType::Ping as u8) => Ok(Event::Ping()),
i if i == (EventType::Pong as u8) => Ok(Event::Pong()),
i if i == (EventType::Disconnect as u8) => Ok(Event::Disconnect()),
_ => Err(anyhow!(ProtocolError {
msg: format!("invalid event_id {}", event_id),
})),
}
}
}
impl Into<Vec<u8>> for &PointerEvent {
fn into(self) -> Vec<u8> {
let id = vec![self.event_type() as u8];
let data = match self {
impl From<&PointerEvent> for Vec<u8> {
fn from(event: &PointerEvent) -> Self {
let id = vec![event.event_type() as u8];
let data = match event {
PointerEvent::Motion {
time,
relative_x,
@@ -214,7 +273,7 @@ impl Into<Vec<u8>> for &PointerEvent {
let time = time.to_be_bytes();
let relative_x = relative_x.to_be_bytes();
let relative_y = relative_y.to_be_bytes();
vec![&time[..], &relative_x[..], &relative_y[..]].concat()
[&time[..], &relative_x[..], &relative_y[..]].concat()
}
PointerEvent::Button {
time,
@@ -224,26 +283,26 @@ impl Into<Vec<u8>> for &PointerEvent {
let time = time.to_be_bytes();
let button = button.to_be_bytes();
let state = state.to_be_bytes();
vec![&time[..], &button[..], &state[..]].concat()
[&time[..], &button[..], &state[..]].concat()
}
PointerEvent::Axis { time, axis, value } => {
let time = time.to_be_bytes();
let axis = axis.to_be_bytes();
let value = value.to_be_bytes();
vec![&time[..], &axis[..], &value[..]].concat()
[&time[..], &axis[..], &value[..]].concat()
}
PointerEvent::Frame {} => {
vec![]
}
};
vec![id, data].concat()
[id, data].concat()
}
}
impl TryFrom<Vec<u8>> for PointerEvent {
type Error = Box<dyn Error>;
type Error = anyhow::Error;
fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
fn try_from(data: Vec<u8>) -> Result<Self> {
match data.get(1) {
Some(id) => {
let event_type = match id.to_owned().try_into() {
@@ -251,11 +310,11 @@ impl TryFrom<Vec<u8>> for PointerEvent {
Err(e) => return Err(e),
};
match event_type {
PointerEventType::MOTION => {
PointerEventType::Motion => {
let time = match data.get(2..6) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
return Err(anyhow!(ProtocolError {
msg: "Expected 4 Bytes at index 2".into(),
}))
}
@@ -263,7 +322,7 @@ impl TryFrom<Vec<u8>> for PointerEvent {
let relative_x = match data.get(6..14) {
Some(d) => f64::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
return Err(anyhow!(ProtocolError {
msg: "Expected 8 Bytes at index 6".into(),
}))
}
@@ -271,7 +330,7 @@ impl TryFrom<Vec<u8>> for PointerEvent {
let relative_y = match data.get(14..22) {
Some(d) => f64::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
return Err(anyhow!(ProtocolError {
msg: "Expected 8 Bytes at index 14".into(),
}))
}
@@ -282,11 +341,11 @@ impl TryFrom<Vec<u8>> for PointerEvent {
relative_y,
})
}
PointerEventType::BUTTON => {
PointerEventType::Button => {
let time = match data.get(2..6) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
return Err(anyhow!(ProtocolError {
msg: "Expected 4 Bytes at index 2".into(),
}))
}
@@ -294,7 +353,7 @@ impl TryFrom<Vec<u8>> for PointerEvent {
let button = match data.get(6..10) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
return Err(anyhow!(ProtocolError {
msg: "Expected 4 Bytes at index 10".into(),
}))
}
@@ -302,7 +361,7 @@ impl TryFrom<Vec<u8>> for PointerEvent {
let state = match data.get(10..14) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
return Err(anyhow!(ProtocolError {
msg: "Expected 4 Bytes at index 14".into(),
}))
}
@@ -313,11 +372,11 @@ impl TryFrom<Vec<u8>> for PointerEvent {
state,
})
}
PointerEventType::AXIS => {
PointerEventType::Axis => {
let time = match data.get(2..6) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
return Err(anyhow!(ProtocolError {
msg: "Expected 4 Bytes at index 2".into(),
}))
}
@@ -325,7 +384,7 @@ impl TryFrom<Vec<u8>> for PointerEvent {
let axis = match data.get(6) {
Some(d) => *d,
None => {
return Err(Box::new(ProtocolError {
return Err(anyhow!(ProtocolError {
msg: "Expected 1 Byte at index 6".into(),
}));
}
@@ -333,32 +392,32 @@ impl TryFrom<Vec<u8>> for PointerEvent {
let value = match data.get(7..15) {
Some(d) => f64::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
return Err(anyhow!(ProtocolError {
msg: "Expected 8 Bytes at index 7".into(),
}));
}
};
Ok(Self::Axis { time, axis, value })
}
PointerEventType::FRAME => Ok(Self::Frame {}),
PointerEventType::Frame => Ok(Self::Frame {}),
}
}
None => Err(Box::new(ProtocolError {
None => Err(anyhow!(ProtocolError {
msg: "Expected an element at index 0".into(),
})),
}
}
}
impl Into<Vec<u8>> for &KeyboardEvent {
fn into(self) -> Vec<u8> {
let id = vec![self.event_type() as u8];
let data = match self {
impl From<&KeyboardEvent> for Vec<u8> {
fn from(event: &KeyboardEvent) -> Self {
let id = vec![event.event_type() as u8];
let data = match event {
KeyboardEvent::Key { time, key, state } => {
let time = time.to_be_bytes();
let key = key.to_be_bytes();
let state = state.to_be_bytes();
vec![&time[..], &key[..], &state[..]].concat()
[&time[..], &key[..], &state[..]].concat()
}
KeyboardEvent::Modifiers {
mods_depressed,
@@ -370,7 +429,7 @@ impl Into<Vec<u8>> for &KeyboardEvent {
let mods_latched = mods_latched.to_be_bytes();
let mods_locked = mods_locked.to_be_bytes();
let group = group.to_be_bytes();
vec![
[
&mods_depressed[..],
&mods_latched[..],
&mods_locked[..],
@@ -379,14 +438,14 @@ impl Into<Vec<u8>> for &KeyboardEvent {
.concat()
}
};
vec![id, data].concat()
[id, data].concat()
}
}
impl TryFrom<Vec<u8>> for KeyboardEvent {
type Error = Box<dyn Error>;
type Error = anyhow::Error;
fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
fn try_from(data: Vec<u8>) -> Result<Self> {
match data.get(1) {
Some(id) => {
let event_type = match id.to_owned().try_into() {
@@ -394,11 +453,11 @@ impl TryFrom<Vec<u8>> for KeyboardEvent {
Err(e) => return Err(e),
};
match event_type {
KeyboardEventType::KEY => {
KeyboardEventType::Key => {
let time = match data.get(2..6) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
return Err(anyhow!(ProtocolError {
msg: "Expected 4 Bytes at index 6".into(),
}))
}
@@ -406,7 +465,7 @@ impl TryFrom<Vec<u8>> for KeyboardEvent {
let key = match data.get(6..10) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
return Err(anyhow!(ProtocolError {
msg: "Expected 4 Bytes at index 10".into(),
}))
}
@@ -414,18 +473,18 @@ impl TryFrom<Vec<u8>> for KeyboardEvent {
let state = match data.get(10) {
Some(d) => *d,
None => {
return Err(Box::new(ProtocolError {
return Err(anyhow!(ProtocolError {
msg: "Expected 1 Bytes at index 14".into(),
}))
}
};
Ok(KeyboardEvent::Key { time, key, state })
}
KeyboardEventType::MODIFIERS => {
KeyboardEventType::Modifiers => {
let mods_depressed = match data.get(2..6) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
return Err(anyhow!(ProtocolError {
msg: "Expected 4 Bytes at index 6".into(),
}))
}
@@ -433,7 +492,7 @@ impl TryFrom<Vec<u8>> for KeyboardEvent {
let mods_latched = match data.get(6..10) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
return Err(anyhow!(ProtocolError {
msg: "Expected 4 Bytes at index 10".into(),
}))
}
@@ -441,7 +500,7 @@ impl TryFrom<Vec<u8>> for KeyboardEvent {
let mods_locked = match data.get(10..14) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
return Err(anyhow!(ProtocolError {
msg: "Expected 4 Bytes at index 14".into(),
}))
}
@@ -449,7 +508,7 @@ impl TryFrom<Vec<u8>> for KeyboardEvent {
let group = match data.get(14..18) {
Some(d) => u32::from_be_bytes(d.try_into()?),
None => {
return Err(Box::new(ProtocolError {
return Err(anyhow!(ProtocolError {
msg: "Expected 4 Bytes at index 18".into(),
}))
}
@@ -463,7 +522,7 @@ impl TryFrom<Vec<u8>> for KeyboardEvent {
}
}
}
None => Err(Box::new(ProtocolError {
None => Err(anyhow!(ProtocolError {
msg: "Expected an element at index 0".into(),
})),
}

View File

@@ -1,322 +0,0 @@
use std::{error::Error, io::Result, collections::HashSet, time::Duration};
use log;
use mio::{Events, Poll, Interest, Token, net::UdpSocket};
#[cfg(not(windows))]
use mio_signals::{Signals, Signal, SignalSet};
use std::{net::SocketAddr, io::ErrorKind};
use crate::{client::{ClientEvent, ClientManager, Position}, consumer::EventConsumer, producer::EventProducer, frontend::{FrontendEvent, FrontendAdapter}, dns};
use super::Event;
/// keeps track of state to prevent a feedback loop
/// of continuously sending and receiving the same event.
#[derive(Eq, PartialEq)]
enum State {
Sending,
Receiving,
}
pub struct Server {
poll: Poll,
socket: UdpSocket,
producer: Box<dyn EventProducer>,
consumer: Box<dyn EventConsumer>,
#[cfg(not(windows))]
signals: Signals,
frontend: FrontendAdapter,
client_manager: ClientManager,
state: State,
}
const UDP_RX: Token = Token(0);
const FRONTEND_RX: Token = Token(1);
const PRODUCER_RX: Token = Token(2);
#[cfg(not(windows))]
const SIGNAL: Token = Token(3);
impl Server {
pub fn new(
port: u16,
mut producer: Box<dyn EventProducer>,
consumer: Box<dyn EventConsumer>,
mut frontend: FrontendAdapter,
) -> Result<Self> {
// bind the udp socket
let listen_addr = SocketAddr::new("0.0.0.0".parse().unwrap(), port);
let mut socket = UdpSocket::bind(listen_addr)?;
// register event sources
let poll = Poll::new()?;
// hand signal handling over to the event loop
#[cfg(not(windows))]
let mut signals = Signals::new(SignalSet::all())?;
#[cfg(not(windows))]
poll.registry().register(&mut signals, SIGNAL, Interest::READABLE)?;
poll.registry().register(&mut socket, UDP_RX, Interest::READABLE | Interest::WRITABLE)?;
poll.registry().register(&mut producer, PRODUCER_RX, Interest::READABLE)?;
poll.registry().register(&mut frontend, FRONTEND_RX, Interest::READABLE)?;
// create client manager
let client_manager = ClientManager::new();
Ok(Server {
poll, socket, consumer, producer,
#[cfg(not(windows))]
signals, frontend,
client_manager,
state: State::Receiving,
})
}
pub fn run(&mut self) -> Result<()> {
let mut events = Events::with_capacity(10);
loop {
match self.poll.poll(&mut events, None) {
Ok(()) => (),
Err(e) if e.kind() == ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
for event in &events {
if !event.is_readable() { continue }
match event.token() {
UDP_RX => self.handle_udp_rx(),
PRODUCER_RX => self.handle_producer_rx(),
FRONTEND_RX => if self.handle_frontend_rx() { return Ok(()) },
#[cfg(not(windows))]
SIGNAL => if self.handle_signal() { return Ok(()) },
_ => panic!("what happened here?")
}
}
}
}
pub fn add_client(&mut self, addr: HashSet<SocketAddr>, pos: Position) {
let client = self.client_manager.add_client(addr, pos);
log::debug!("add_client {client}");
self.producer.notify(ClientEvent::Create(client, pos));
self.consumer.notify(ClientEvent::Create(client, pos));
}
pub fn remove_client(&mut self, host: String, port: u16) {
if let Ok(ips) = dns::resolve(host.as_str()) {
if let Some(ip) = ips.iter().next() {
let addr = SocketAddr::new(*ip, port);
if let Some(handle) = self.client_manager.get_client(addr) {
log::debug!("remove_client {handle}");
self.client_manager.remove_client(handle);
self.producer.notify(ClientEvent::Destroy(handle));
self.consumer.notify(ClientEvent::Destroy(handle));
}
}
}
}
fn handle_udp_rx(&mut self) {
loop {
let (event, addr) = match self.receive_event() {
Ok(e) => e,
Err(e) => {
if e.is::<std::io::Error>() {
if let ErrorKind::WouldBlock = e.downcast_ref::<std::io::Error>()
.unwrap()
.kind() {
return
}
}
log::error!("{}", e);
continue
}
};
log::trace!("{:20} <-<-<-<------ {addr}", event.to_string());
// get handle for addr
let handle = match self.client_manager.get_client(addr) {
Some(a) => a,
None => {
log::warn!("ignoring event from client {addr:?}");
continue
}
};
// reset ttl for client and set addr as new default for this client
self.client_manager.reset_last_seen(handle);
self.client_manager.set_default_addr(handle, addr);
match (event, addr) {
(Event::Pong(), _) => {},
(Event::Ping(), addr) => {
if let Err(e) = Self::send_event(&self.socket, Event::Pong(), addr) {
log::error!("udp send: {}", e);
}
// we release the mouse here,
// since its very likely, that we wont get a release event
self.producer.release();
}
(event, addr) => {
match self.state {
State::Sending => {
// in sending state, we dont want to process
// any events to avoid feedback loops,
// therefore we tell the event producer
// to release the pointer and move on
// first event -> release pointer
if let Event::Release() = event {
log::debug!("releasing pointer ...");
self.producer.release();
self.state = State::Receiving;
}
}
State::Receiving => {
// consume event
self.consumer.consume(event, handle);
// let the server know we are still alive once every second
let last_replied = self.client_manager.last_replied(handle);
if last_replied.is_none()
|| last_replied.is_some() && last_replied.unwrap() > Duration::from_secs(1) {
self.client_manager.reset_last_replied(handle);
if let Err(e) = Self::send_event(&self.socket, Event::Pong(), addr) {
log::error!("udp send: {}", e);
}
}
}
}
}
}
}
}
fn handle_producer_rx(&mut self) {
let events = self.producer.read_events();
let mut should_release = false;
for (c, e) in events.into_iter() {
// in receiving state, only release events
// must be transmitted
if let Event::Release() = e {
self.state = State::Sending;
}
// otherwise we should have an address to send to
// transmit events to the corrensponding client
if let Some(addr) = self.client_manager.get_active_addr(c) {
log::trace!("{:20} ------>->->-> {addr}", e.to_string());
if let Err(e) = Self::send_event(&self.socket, e, addr) {
log::error!("udp send: {}", e);
}
}
// if client last responded > 2 seconds ago
// and we have not sent a ping since 500 milliseconds,
// send a ping
let last_seen = self.client_manager.last_seen(c);
let last_ping = self.client_manager.last_ping(c);
if last_seen.is_some() && last_seen.unwrap() < Duration::from_secs(2) {
continue
}
// client last seen > 500ms ago
if last_ping.is_some() && last_ping.unwrap() < Duration::from_millis(500) {
continue
}
// release mouse if client didnt respond to the first ping
if last_ping.is_some() && last_ping.unwrap() < Duration::from_secs(1) {
should_release = true;
}
// last ping > 500ms ago -> ping all interfaces
self.client_manager.reset_last_ping(c);
if let Some(iter) = self.client_manager.get_addrs(c) {
for addr in iter {
log::debug!("pinging {addr}");
if let Err(e) = Self::send_event(&self.socket, Event::Ping(), addr) {
if e.kind() != ErrorKind::WouldBlock {
log::error!("udp send: {}", e);
}
}
// send additional release event, in case client is still in sending mode
if let Err(e) = Self::send_event(&self.socket, Event::Release(), addr) {
if e.kind() != ErrorKind::WouldBlock {
log::error!("udp send: {}", e);
}
}
}
} else {
// TODO should repeat dns lookup
}
}
if should_release && self.state != State::Receiving {
log::info!("client not responding - releasing pointer");
self.producer.release();
self.state = State::Receiving;
}
}
fn handle_frontend_rx(&mut self) -> bool {
loop {
match self.frontend.read_event() {
Ok(event) => match event {
FrontendEvent::AddClient(host, port, pos) => {
if let Ok(ips) = dns::resolve(host.as_str()) {
let addrs = ips.iter().map(|i| SocketAddr::new(*i, port));
self.add_client(HashSet::from_iter(addrs), pos);
}
}
FrontendEvent::DelClient(host, port) => self.remove_client(host, port),
FrontendEvent::Shutdown() => {
log::info!("terminating gracefully...");
return true;
},
FrontendEvent::ChangePort(_) => todo!(),
FrontendEvent::AddIp(_, _) => todo!(),
}
Err(e) if e.kind() == ErrorKind::WouldBlock => return false,
Err(e) => {
log::error!("frontend: {e}");
}
}
}
}
#[cfg(not(windows))]
fn handle_signal(&mut self) -> bool {
#[cfg(windows)]
return false;
#[cfg(not(windows))]
loop {
match self.signals.receive() {
Err(e) if e.kind() == ErrorKind::WouldBlock => return false,
Err(e) => {
log::error!("error reading signal: {e}");
return false;
}
Ok(Some(Signal::Interrupt) | Some(Signal::Terminate)) => {
// terminate on SIG_INT or SIG_TERM
log::info!("terminating gracefully...");
return true;
},
Ok(Some(signal)) => {
log::info!("ignoring signal {signal:?}");
},
Ok(None) => return false,
}
}
}
fn send_event(sock: &UdpSocket, e: Event, addr: SocketAddr) -> Result<usize> {
let data: Vec<u8> = (&e).into();
// We are currently abusing a blocking send to get the lowest possible latency.
// It may be better to set the socket to non-blocking and only send when ready.
sock.send_to(&data[..], addr)
}
fn receive_event(&self) -> std::result::Result<(Event, SocketAddr), Box<dyn Error>> {
let mut buf = vec![0u8; 22];
match self.socket.recv_from(&mut buf) {
Ok((_amt, src)) => Ok((Event::try_from(buf)?, src)),
Err(e) => Err(Box::new(e)),
}
}
}

View File

@@ -1,121 +1,272 @@
use std::io::{Read, Result};
use std::net::IpAddr;
use std::str;
use anyhow::{anyhow, Result};
use std::{cmp::min, io::ErrorKind, str, time::Duration};
#[cfg(unix)]
use std::{env, path::{Path, PathBuf}};
use std::{
env,
path::{Path, PathBuf},
};
use mio::{Registry, Token, event::Source};
use tokio::io::ReadHalf;
use tokio::io::{AsyncReadExt, AsyncWriteExt, WriteHalf};
#[cfg(unix)]
use mio::net::UnixListener;
use tokio::net::UnixListener;
#[cfg(unix)]
use tokio::net::UnixStream;
#[cfg(windows)]
use mio::net::TcpListener;
use tokio::net::TcpListener;
#[cfg(windows)]
use tokio::net::TcpStream;
use serde::{Serialize, Deserialize};
use serde::{Deserialize, Serialize};
use crate::client::{Client, Position};
use crate::{
client::{Client, ClientHandle, Position},
config::{Config, Frontend},
};
/// cli frontend
pub mod cli;
/// gtk frontend
#[cfg(all(unix, feature = "gtk"))]
#[cfg(feature = "gtk")]
pub mod gtk;
pub fn run_frontend(config: &Config) -> Result<()> {
match config.frontend {
#[cfg(feature = "gtk")]
Frontend::Gtk => {
gtk::run();
}
#[cfg(not(feature = "gtk"))]
Frontend::Gtk => panic!("gtk frontend requested but feature not enabled!"),
Frontend::Cli => {
cli::run()?;
}
};
Ok(())
}
fn exponential_back_off(duration: &mut Duration) -> &Duration {
let new = duration.saturating_mul(2);
*duration = min(new, Duration::from_secs(1));
duration
}
/// wait for the lan-mouse socket to come online
#[cfg(unix)]
pub fn wait_for_service() -> Result<std::os::unix::net::UnixStream> {
let socket_path = FrontendListener::socket_path()?;
let mut duration = Duration::from_millis(1);
loop {
use std::os::unix::net::UnixStream;
if let Ok(stream) = UnixStream::connect(&socket_path) {
break Ok(stream);
}
// a signaling mechanism or inotify could be used to
// improve this
std::thread::sleep(*exponential_back_off(&mut duration));
}
}
#[cfg(windows)]
pub fn wait_for_service() -> Result<std::net::TcpStream> {
let mut duration = Duration::from_millis(1);
loop {
use std::net::TcpStream;
if let Ok(stream) = TcpStream::connect("127.0.0.1:5252") {
break Ok(stream);
}
std::thread::sleep(*exponential_back_off(&mut duration));
}
}
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub enum FrontendEvent {
/// add a new client
AddClient(Option<String>, u16, Position),
/// activate/deactivate client
ActivateClient(ClientHandle, bool),
/// change the listen port (recreate udp listener)
ChangePort(u16),
AddClient(String, u16, Position),
DelClient(String, u16),
AddIp(String, Option<IpAddr>),
/// 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 {
NotifyClientActivate(ClientHandle, bool),
NotifyClientCreate(Client),
NotifyClientUpdate(Client),
NotifyClientDelete(ClientHandle),
/// new port, reason of failure (if failed)
NotifyPortChange(u16, Option<String>),
/// Client State, active
Enumerate(Vec<(Client, bool)>),
NotifyError(String),
}
pub struct FrontendAdapter {
pub struct FrontendListener {
#[cfg(windows)]
listener: TcpListener,
#[cfg(unix)]
listener: UnixListener,
#[cfg(unix)]
socket_path: PathBuf,
#[cfg(unix)]
tx_streams: Vec<WriteHalf<UnixStream>>,
#[cfg(windows)]
tx_streams: Vec<WriteHalf<TcpStream>>,
}
impl FrontendAdapter {
pub fn new() -> std::result::Result<Self, Box<dyn std::error::Error>> {
impl FrontendListener {
#[cfg(all(unix, not(target_os = "macos")))]
pub fn socket_path() -> Result<PathBuf> {
let xdg_runtime_dir = match env::var("XDG_RUNTIME_DIR") {
Ok(d) => d,
Err(e) => return Err(anyhow!("could not find XDG_RUNTIME_DIR: {e}")),
};
let xdg_runtime_dir = Path::new(xdg_runtime_dir.as_str());
Ok(xdg_runtime_dir.join("lan-mouse-socket.sock"))
}
#[cfg(all(unix, target_os = "macos"))]
pub fn socket_path() -> Result<PathBuf> {
let home = match env::var("HOME") {
Ok(d) => d,
Err(e) => return Err(anyhow!("could not find HOME: {e}")),
};
let home = Path::new(home.as_str());
let path = home
.join("Library")
.join("Caches")
.join("lan-mouse-socket.sock");
Ok(path)
}
pub async fn new() -> Option<Result<Self>> {
#[cfg(unix)]
let socket_path = Path::new(env::var("XDG_RUNTIME_DIR")?.as_str()).join("lan-mouse-socket.sock");
#[cfg(unix)]
log::debug!("remove socket: {:?}", socket_path);
#[cfg(unix)]
if socket_path.exists() {
std::fs::remove_file(&socket_path).unwrap();
}
#[cfg(unix)]
let listener = UnixListener::bind(&socket_path)?;
let (socket_path, listener) = {
let socket_path = match Self::socket_path() {
Ok(path) => path,
Err(e) => return Some(Err(e)),
};
log::debug!("remove socket: {:?}", socket_path);
if socket_path.exists() {
// try to connect to see if some other instance
// of lan-mouse is already running
match UnixStream::connect(&socket_path).await {
// connected -> lan-mouse is already running
Ok(_) => return None,
// lan-mouse is not running but a socket was left behind
Err(e) => {
log::debug!("{socket_path:?}: {e} - removing left behind socket");
let _ = std::fs::remove_file(&socket_path);
}
}
}
let listener = match UnixListener::bind(&socket_path) {
Ok(ls) => ls,
// some other lan-mouse instance has bound the socket in the meantime
Err(e) if e.kind() == ErrorKind::AddrInUse => return None,
Err(e) => return Some(Err(anyhow!("failed to bind lan-mouse-socket: {e}"))),
};
(socket_path, listener)
};
#[cfg(windows)]
let listener = TcpListener::bind("127.0.0.1:5252".parse().unwrap())?; // abuse tcp
let listener = match TcpListener::bind("127.0.0.1:5252").await {
Ok(ls) => ls,
// some other lan-mouse instance has bound the socket in the meantime
Err(e) if e.kind() == ErrorKind::AddrInUse => return None,
Err(e) => return Some(Err(anyhow!("failed to bind lan-mouse-socket: {e}"))),
};
let adapter = Self {
listener,
#[cfg(unix)]
socket_path,
tx_streams: vec![],
};
Ok(adapter)
Some(Ok(adapter))
}
pub fn read_event(&mut self) -> Result<FrontendEvent>{
let (mut stream, _) = self.listener.accept()?;
let mut buf = [0u8; 128]; // FIXME
stream.read(&mut buf)?;
let json = str::from_utf8(&buf)
.unwrap()
.trim_end_matches(char::from(0)); // remove trailing 0-bytes
log::debug!("{json}");
let event = serde_json::from_str(json).unwrap();
log::debug!("{:?}", event);
Ok(event)
#[cfg(unix)]
pub async fn accept(&mut self) -> Result<ReadHalf<UnixStream>> {
let stream = self.listener.accept().await?.0;
let (rx, tx) = tokio::io::split(stream);
self.tx_streams.push(tx);
Ok(rx)
}
pub fn notify(&self, _event: FrontendNotify) { }
}
impl Source for FrontendAdapter {
fn register(
&mut self,
registry: &Registry,
token: Token,
interests: mio::Interest,
) -> Result<()> {
self.listener.register(registry, token, interests)
#[cfg(windows)]
pub async fn accept(&mut self) -> Result<ReadHalf<TcpStream>> {
let stream = self.listener.accept().await?.0;
let (rx, tx) = tokio::io::split(stream);
self.tx_streams.push(tx);
Ok(rx)
}
fn reregister(
&mut self,
registry: &Registry,
token: Token,
interests: mio::Interest,
) -> Result<()> {
self.listener.reregister(registry, token, interests)
}
pub(crate) async fn notify_all(&mut self, notify: FrontendNotify) -> Result<()> {
// encode event
let json = serde_json::to_string(&notify).unwrap();
let payload = json.as_bytes();
let len = payload.len().to_be_bytes();
log::debug!("json: {json}, len: {}", payload.len());
fn deregister(&mut self, registry: &Registry) -> Result<()> {
self.listener.deregister(registry)
let mut keep = vec![];
// TODO do simultaneously
for tx in self.tx_streams.iter_mut() {
// write len + payload
if tx.write(&len).await.is_err() {
keep.push(false);
continue;
}
if tx.write(payload).await.is_err() {
keep.push(false);
continue;
}
keep.push(true);
}
// could not find a better solution because async
let mut keep = keep.into_iter();
self.tx_streams.retain(|_| keep.next().unwrap());
Ok(())
}
}
#[cfg(unix)]
impl Drop for FrontendAdapter {
impl Drop for FrontendListener {
fn drop(&mut self) {
log::debug!("remove socket: {:?}", self.socket_path);
std::fs::remove_file(&self.socket_path).unwrap();
let _ = std::fs::remove_file(&self.socket_path);
}
}
#[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])?)
}

View File

@@ -1,99 +1,242 @@
use anyhow::Result;
use std::{thread::{self, JoinHandle}, io::Write};
#[cfg(windows)]
use std::net::SocketAddrV4;
use anyhow::{anyhow, Context, Result};
#[cfg(unix)]
use std::{os::unix::net::UnixStream, path::Path, env};
#[cfg(windows)]
use std::net::TcpStream;
use std::{
io::{ErrorKind, Read, Write},
str::SplitWhitespace,
thread,
};
use crate::{client::Position, config::DEFAULT_PORT};
use super::FrontendEvent;
use super::{FrontendEvent, FrontendNotify};
pub fn start() -> Result<JoinHandle<()>> {
#[cfg(unix)]
let socket_path = Path::new(env::var("XDG_RUNTIME_DIR")?.as_str()).join("lan-mouse-socket.sock");
Ok(thread::Builder::new()
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 || {
loop {
eprint!("lan-mouse > ");
std::io::stderr().flush().unwrap();
let mut buf = String::new();
match std::io::stdin().read_line(&mut buf) {
Ok(len) => {
if let Some(event) = parse_cmd(buf, len) {
#[cfg(unix)]
let Ok(mut stream) = UnixStream::connect(&socket_path) else {
log::error!("Could not connect to lan-mouse-socket");
continue;
};
#[cfg(windows)]
let Ok(mut stream) = TcpStream::connect("127.0.0.1:5252".parse::<SocketAddrV4>().unwrap()) else {
log::error!("Could not connect to lan-mouse-server");
continue;
};
let json = serde_json::to_string(&event).unwrap();
if let Err(e) = stream.write(json.as_bytes()) {
log::error!("error sending message: {e}");
};
if event == FrontendEvent::Shutdown() {
break;
// all further prompts
prompt();
loop {
let mut buf = String::new();
match std::io::stdin().read_line(&mut buf) {
Ok(0) => return,
Ok(len) => {
if let Some(events) = parse_cmd(buf, len) {
for event in events.iter() {
let json = serde_json::to_string(&event).unwrap();
let bytes = json.as_bytes();
let len = bytes.len().to_be_bytes();
if let Err(e) = tx.write(&len) {
log::error!("error sending message: {e}");
};
if let Err(e) = tx.write(bytes) {
log::error!("error sending message: {e}");
};
if *event == FrontendEvent::Shutdown() {
return;
}
}
// prompt is printed after the server response is received
} else {
prompt();
}
}
}
Err(e) => {
log::error!("{e:?}");
break
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::NotifyClientActivate(handle, active) => {
if active {
log::info!("client {handle} activated");
} else {
log::info!("client {handle} deactivated");
}
}
FrontendNotify::NotifyClientCreate(client) => {
let handle = client.handle;
let port = client.port;
let pos = client.pos;
let hostname = client.hostname.as_deref().unwrap_or("");
log::info!("new client ({handle}): {hostname}:{port} - {pos}");
}
FrontendNotify::NotifyClientUpdate(client) => {
let handle = client.handle;
let port = client.port;
let pos = client.pos;
let hostname = client.hostname.as_deref().unwrap_or("");
log::info!("client ({handle}) updated: {hostname}:{port} - {pos}");
}
FrontendNotify::NotifyClientDelete(client) => {
log::info!("client ({client}) deleted.");
}
FrontendNotify::NotifyError(e) => {
log::warn!("{e}");
}
FrontendNotify::Enumerate(clients) => {
for (client, active) in clients.into_iter() {
log::info!(
"client ({}) [{}]: active: {}, associated addresses: [{}]",
client.handle,
client.hostname.as_deref().unwrap_or(""),
if active { "yes" } else { "no" },
client
.ips
.into_iter()
.map(|a| a.to_string())
.collect::<Vec<String>>()
.join(", ")
);
}
}
FrontendNotify::NotifyPortChange(port, msg) => match msg {
Some(msg) => log::info!("could not change port: {msg}"),
None => log::info!("port changed: {port}"),
},
}
prompt();
}
})?;
match reader.join() {
Ok(_) => {}
Err(e) => {
let msg = match (e.downcast_ref::<&str>(), e.downcast_ref::<String>()) {
(Some(&s), _) => s,
(_, Some(s)) => s,
_ => "no panic info",
};
log::error!("reader thread paniced: {msg}");
}
})?)
}
Ok(())
}
fn parse_cmd(s: String, len: usize) -> Option<FrontendEvent> {
fn prompt() {
eprint!("lan-mouse > ");
std::io::stderr().flush().unwrap();
}
fn parse_cmd(s: String, len: usize) -> Option<Vec<FrontendEvent>> {
if len == 0 {
return Some(FrontendEvent::Shutdown())
return Some(vec![FrontendEvent::Shutdown()]);
}
let mut l = s.split_whitespace();
let cmd = l.next()?;
match cmd {
"connect" => {
let host = l.next()?.to_owned();
let pos = match l.next()? {
"right" => Position::Right,
"top" => Position::Top,
"bottom" => Position::Bottom,
_ => Position::Left,
};
let port = match l.next() {
Some(p) => match p.parse() {
Ok(p) => p,
Err(e) => {
log::error!("{e}");
return None;
}
}
None => DEFAULT_PORT,
};
Some(FrontendEvent::AddClient(host, port, pos))
}
"disconnect" => {
let host = l.next()?.to_owned();
let port = match l.next()?.parse() {
Ok(p) => p,
Err(e) => {
log::error!("{e}");
return None;
}
};
Some(FrontendEvent::DelClient(host, port))
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)])
}

View File

@@ -1,93 +1,171 @@
mod window;
mod client_object;
mod client_row;
mod window;
use std::{io::Result, thread::{self, JoinHandle}};
use std::{
env,
io::{ErrorKind, Read},
process, str,
};
use crate::frontend::gtk::window::Window;
use gtk::{prelude::*, IconTheme, gdk::Display, gio::{SimpleAction, SimpleActionGroup}, glib::clone, CssProvider};
use adw::Application;
use gtk::{
gdk::Display, glib::clone, prelude::*, subclass::prelude::ObjectSubclassIsExt, CssProvider,
IconTheme,
};
use gtk::{gio, glib, prelude::ApplicationExt};
use self::client_object::ClientObject;
pub fn start() -> Result<JoinHandle<glib::ExitCode>> {
thread::Builder::new()
.name("gtk-thread".into())
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();
if ret == glib::ExitCode::FAILURE {
log::error!("frontend exited with failure");
} else {
log::info!("frontend exited successfully");
}
ret
}
fn gtk_main() -> glib::ExitCode {
gio::resources_register_include!("lan-mouse.gresource")
.expect("Failed to register resources.");
gio::resources_register_include!("lan-mouse.gresource").expect("Failed to register resources.");
let app = Application::builder()
.application_id("de.feschber.lan-mouse")
.application_id("de.feschber.LanMouse")
.build();
app.connect_startup(|_| load_icons());
app.connect_startup(|_| load_css());
app.connect_activate(build_ui);
app.run()
let args: Vec<&'static str> = vec![];
app.run_with_args(&args)
}
fn load_css() {
let provider = CssProvider::new();
provider.load_from_resource("de/feschber/LanMouse/style.css");
gtk::style_context_add_provider_for_display(
&Display::default().expect("Could not connect to a display."),
&Display::default().expect("Could not connect to a display."),
&provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}
fn load_icons() {
let icon_theme = IconTheme::for_display(&Display::default().expect("Could not connect to a display."));
let display = &Display::default().expect("Could not connect to a display.");
let icon_theme = IconTheme::for_display(display);
icon_theme.add_resource_path("/de/feschber/LanMouse/icons");
}
fn build_ui(app: &Application) {
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);
let action_client_activate = SimpleAction::new(
"activate-client",
Some(&i32::static_variant_type()),
);
let action_client_delete = SimpleAction::new(
"delete-client",
Some(&i32::static_variant_type()),
);
action_client_activate.connect_activate(clone!(@weak window => move |_action, param| {
log::debug!("activate-client");
let index = param.unwrap()
.get::<i32>()
.unwrap();
let Some(client) = window.clients().item(index as u32) else {
return;
};
let client = client.downcast_ref::<ClientObject>().unwrap();
window.update_client(client);
}));
action_client_delete.connect_activate(clone!(@weak window => move |_action, param| {
log::debug!("delete-client");
let index = param.unwrap()
.get::<i32>()
.unwrap();
let Some(client) = window.clients().item(index as u32) else {
return;
};
let client = client.downcast_ref::<ClientObject>().unwrap();
window.update_client(client);
window.clients().remove(index as u32);
if window.clients().n_items() == 0 {
window.set_placeholder_visible(true);
window.imp().stream.borrow_mut().replace(tx);
glib::spawn_future_local(clone!(@weak window => async move {
loop {
let notify = receiver.recv().await.unwrap_or_else(|_| process::exit(1));
match notify {
FrontendNotify::NotifyClientActivate(handle, active) => {
window.activate_client(handle, active);
}
FrontendNotify::NotifyClientCreate(client) => {
window.new_client(client, false);
},
FrontendNotify::NotifyClientUpdate(client) => {
window.update_client(client);
}
FrontendNotify::NotifyError(e) => {
window.show_toast(e.as_str());
},
FrontendNotify::NotifyClientDelete(client) => {
window.delete_client(client);
}
FrontendNotify::Enumerate(clients) => {
for (client, active) in clients {
if window.client_idx(client.handle).is_some() {
window.activate_client(client.handle, active);
window.update_client(client);
} else {
window.new_client(client, 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 actions = SimpleActionGroup::new();
window.insert_action_group("win", Some(&actions));
actions.add_action(&action_client_activate);
actions.add_action(&action_client_delete);
window.present();
}

View File

@@ -1,19 +1,22 @@
mod imp;
use gtk::glib::{self, Object};
use adw::subclass::prelude::*;
use gtk::glib::{self, Object};
use crate::client::{Client, ClientHandle};
glib::wrapper! {
pub struct ClientObject(ObjectSubclass<imp::ClientObject>);
}
impl ClientObject {
pub fn new(hostname: String, port: u32, active: bool, position: String) -> Self {
pub fn new(client: Client, active: bool) -> Self {
Object::builder()
.property("hostname", hostname)
.property("port", port)
.property("handle", client.handle)
.property("hostname", client.hostname)
.property("port", client.port as u32)
.property("position", client.pos.to_string())
.property("active", active)
.property("position", position)
.build()
}
@@ -24,7 +27,8 @@ impl ClientObject {
#[derive(Default, Clone)]
pub struct ClientData {
pub hostname: String,
pub handle: ClientHandle,
pub hostname: Option<String>,
pub port: u32,
pub active: bool,
pub position: String,

View File

@@ -5,11 +5,14 @@ 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)]

View File

@@ -16,8 +16,7 @@ glib::wrapper! {
impl ClientRow {
pub fn new(_client_object: &ClientObject) -> Self {
Object::builder()
.build()
Object::builder().build()
}
pub fn bind(&self, client_object: &ClientObject) {
@@ -29,10 +28,27 @@ impl ClientRow {
.sync_create()
.build();
let switch_position_binding = client_object
.bind_property("active", &self.imp().enable_switch.get(), "active")
.bidirectional()
.sync_create()
.build();
let hostname_binding = client_object
.bind_property("hostname", &self.imp().hostname.get(), "text")
.transform_to(|_, v: Option<String>| {
if let Some(hostname) = v {
Some(hostname)
} else {
Some("".to_string())
}
})
.transform_from(|_, v: String| {
if v == "" { Some("hostname".into()) } else { Some(v) }
if v.as_str().trim() == "" {
Some(None)
} else {
Some(Some(v))
}
})
.bidirectional()
.sync_create()
@@ -40,18 +56,34 @@ impl ClientRow {
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 == "" {
Some(4242)
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
@@ -59,30 +91,26 @@ impl ClientRow {
.sync_create()
.build();
let position_binding = client_object
.bind_property("position", &self.imp().position.get(), "selected")
.transform_from(|_, v: u32| {
match v {
1 => Some("right"),
2 => Some("top"),
3 => Some("bottom"),
_ => Some("left"),
}
.transform_from(|_, v: u32| match v {
1 => Some("right"),
2 => Some("top"),
3 => Some("bottom"),
_ => Some("left"),
})
.transform_to(|_, v: String| {
match v.as_str() {
"right" => Some(1),
"top" => Some(2u32),
"bottom" => Some(3u32),
_ => Some(0u32),
}
.transform_to(|_, v: String| match v.as_str() {
"right" => Some(1),
"top" => Some(2u32),
"bottom" => Some(3u32),
_ => Some(0u32),
})
.bidirectional()
.sync_create()
.build();
bindings.push(active_binding);
bindings.push(switch_position_binding);
bindings.push(hostname_binding);
bindings.push(title_binding);
bindings.push(port_binding);

View File

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

View File

@@ -1,13 +1,21 @@
mod imp;
use std::{path::{Path, PathBuf}, env, process, os::unix::net::UnixStream, io::Write};
use std::io::Write;
use adw::prelude::*;
use adw::subclass::prelude::*;
use gtk::{glib, gio, NoSelection};
use glib::{clone, Object};
use gtk::{
gio,
glib::{self, closure_local},
NoSelection,
};
use crate::{frontend::{gtk::client_object::ClientObject, FrontendEvent}, config::DEFAULT_PORT, client::Position};
use crate::{
client::{Client, ClientHandle, Position},
config::DEFAULT_PORT,
frontend::{gtk::client_object::ClientObject, FrontendEvent},
};
use super::client_row::ClientRow;
@@ -41,6 +49,18 @@ impl Window {
clone!(@weak self as window => @default-panic, move |obj| {
let client_object = obj.downcast_ref().expect("Expected object of type `ClientObject`.");
let row = window.create_client_row(client_object);
row.connect_closure("request-update", false, closure_local!(@strong window => move |row: ClientRow, active: bool| {
let index = row.index() as u32;
let Some(client) = window.clients().item(index) else {
return;
};
let client = client.downcast_ref::<ClientObject>().unwrap();
window.request_client_update(client, active);
}));
row.connect_closure("request-delete", false, closure_local!(@strong window => move |row: ClientRow| {
let index = row.index() as u32;
window.request_client_delete(index);
}));
row.upcast()
})
);
@@ -58,7 +78,7 @@ impl Window {
}
fn setup_icon(&self) {
self.set_icon_name(Some("mouse-icon"));
self.set_icon_name(Some("de.feschber.LanMouse"));
}
fn create_client_row(&self, client_object: &ClientObject) -> ClientRow {
@@ -67,61 +87,135 @@ impl Window {
row
}
fn new_client(&self) {
let client = ClientObject::new(String::from(""), DEFAULT_PORT as u32, false, "left".into());
pub fn new_client(&self, client: Client, active: bool) {
let client = ClientObject::new(client, active);
self.clients().append(&client);
self.set_placeholder_visible(false);
}
pub fn update_client(&self, client: &ClientObject) {
let data = client.get_data();
let socket_path = self.imp().socket_path.borrow();
let socket_path = socket_path.as_ref().unwrap().as_path();
let host_name = data.hostname;
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
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
}
};
let port = data.port;
let event = if client.active() {
FrontendEvent::DelClient(host_name, port as u16)
} else {
FrontendEvent::AddClient(host_name, port as u16, position)
};
let json = serde_json::to_string(&event).unwrap();
let Ok(mut stream) = UnixStream::connect(socket_path) else {
log::error!("Could not connect to lan-mouse-socket @ {socket_path:?}");
})
}
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;
};
if let Err(e) = stream.write(json.as_bytes()) {
self.clients().remove(idx as u32);
if self.clients().n_items() == 0 {
self.set_placeholder_visible(true);
}
}
pub fn update_client(&self, client: Client) {
let Some(idx) = self.client_idx(client.handle) else {
log::warn!("could not find client with handle {}", client.handle);
return;
};
let client_object = self.clients().item(idx as u32).unwrap();
let client_object: &ClientObject = client_object.downcast_ref().unwrap();
let data = client_object.get_data();
/* only change if it actually has changed, otherwise
* the update signal is triggered */
if data.hostname != client.hostname {
client_object.set_hostname(client.hostname.unwrap_or("".into()));
}
if data.port != client.port as u32 {
client_object.set_port(client.port as u32);
}
if data.position != client.pos.to_string() {
client_object.set_position(client.pos.to_string());
}
}
pub fn activate_client(&self, handle: ClientHandle, active: bool) {
let Some(idx) = self.client_idx(handle) else {
log::warn!("could not find client with handle {handle}");
return;
};
let client_object = self.clients().item(idx as u32).unwrap();
let client_object: &ClientObject = client_object.downcast_ref().unwrap();
let data = client_object.get_data();
if data.active != active {
client_object.set_active(active);
log::debug!("set active to {active}");
}
}
pub fn request_client_create(&self) {
let event = FrontendEvent::AddClient(None, DEFAULT_PORT, Position::default());
self.imp().set_port(DEFAULT_PORT);
self.request(event);
}
pub fn request_port_change(&self) {
let port = self.imp().port_entry.get().text().to_string();
if let Ok(port) = port.as_str().parse::<u16>() {
self.request(FrontendEvent::ChangePort(port));
} else {
self.request(FrontendEvent::ChangePort(DEFAULT_PORT));
}
}
pub fn request_client_update(&self, client: &ClientObject, active: bool) {
let data = client.get_data();
let position = match Position::try_from(data.position.as_str()) {
Ok(pos) => pos,
_ => {
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);
log::debug!("requesting update: {event:?}");
self.request(event);
let event = FrontendEvent::ActivateClient(client.handle(), active);
log::debug!("requesting activate: {event:?}");
self.request(event);
}
pub fn request_client_delete(&self, idx: u32) {
if let Some(obj) = self.clients().item(idx) {
let client_object: &ClientObject = obj
.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}");
};
}
fn setup_callbacks(&self) {
self.imp()
.add_client_button
.connect_clicked(clone!(@weak self as window => move |_| {
window.new_client();
window.set_placeholder_visible(false);
}));
}
fn connect_stream(&self) {
let xdg_runtime_dir = match env::var("XDG_RUNTIME_DIR") {
Ok(v) => v,
Err(e) => {
log::error!("{e}");
process::exit(1);
}
};
let socket_path = Path::new(xdg_runtime_dir.as_str())
.join("lan-mouse-socket.sock");
self.imp().socket_path.borrow_mut().replace(PathBuf::from(socket_path));
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

@@ -1,30 +1,48 @@
use std::{cell::{Cell, RefCell}, path::PathBuf};
use std::cell::{Cell, RefCell};
#[cfg(windows)]
use std::net::TcpStream;
#[cfg(unix)]
use std::os::unix::net::UnixStream;
use glib::subclass::InitializingObject;
use adw::{prelude::*, ActionRow};
use adw::subclass::prelude::*;
use gtk::{glib, Button, CompositeTemplate, ListBox, gio};
use adw::{prelude::*, ActionRow, ToastOverlay};
use glib::subclass::InitializingObject;
use gtk::{gio, glib, Button, CompositeTemplate, Entry, ListBox};
use crate::config::DEFAULT_PORT;
#[derive(CompositeTemplate, Default)]
#[template(resource = "/de/feschber/LanMouse/window.ui")]
pub struct Window {
pub number: Cell<i32>,
#[template_child]
pub add_client_button: TemplateChild<Button>,
pub port_edit_apply: TemplateChild<Button>,
#[template_child]
pub port_edit_cancel: TemplateChild<Button>,
#[template_child]
pub client_list: TemplateChild<ListBox>,
#[template_child]
pub client_placeholder: TemplateChild<ActionRow>,
#[template_child]
pub port_entry: TemplateChild<Entry>,
#[template_child]
pub toast_overlay: TemplateChild<ToastOverlay>,
pub clients: RefCell<Option<gio::ListStore>>,
pub socket_path: RefCell<Option<PathBuf>>,
#[cfg(unix)]
pub stream: RefCell<Option<UnixStream>>,
#[cfg(windows)]
pub stream: RefCell<Option<TcpStream>>,
pub port: Cell<u16>,
}
#[glib::object_subclass]
impl ObjectSubclass for Window {
// `NAME` needs to match `class` attribute of template
const NAME: &'static str = "LanMouseWindow";
const ABSTRACT: bool = false;
type Type = super::Window;
type ParentType = gtk::ApplicationWindow;
type ParentType = adw::ApplicationWindow;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
@@ -39,25 +57,53 @@ impl ObjectSubclass for Window {
#[gtk::template_callbacks]
impl Window {
#[template_callback]
fn handle_button_clicked(&self, button: &Button) {
let number_increased = self.number.get() + 1;
self.number.set(number_increased);
button.set_label(&number_increased.to_string())
fn handle_add_client_pressed(&self, _button: &Button) {
self.obj().request_client_create();
}
#[template_callback]
fn handle_port_changed(&self, _entry: &Entry) {
self.port_edit_apply.set_visible(true);
self.port_edit_cancel.set_visible(true);
}
#[template_callback]
fn handle_port_edit_apply(&self) {
self.obj().request_port_change();
}
#[template_callback]
fn handle_port_edit_cancel(&self) {
log::debug!("cancel port edit");
self.port_entry
.set_text(self.port.get().to_string().as_str());
self.port_edit_apply.set_visible(false);
self.port_edit_cancel.set_visible(false);
}
pub fn set_port(&self, port: u16) {
self.port.set(port);
if port == DEFAULT_PORT {
self.port_entry.set_text("");
} else {
self.port_entry.set_text(format!("{port}").as_str());
}
self.port_edit_apply.set_visible(false);
self.port_edit_cancel.set_visible(false);
}
}
impl ObjectImpl for Window {
fn constructed(&self) {
self.parent_constructed();
self.set_port(DEFAULT_PORT);
let obj = self.obj();
obj.setup_icon();
obj.setup_clients();
obj.setup_callbacks();
obj.connect_stream();
}
}
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,10 @@ pub mod client;
pub mod config;
pub mod dns;
pub mod event;
pub mod server;
pub mod consumer;
pub mod producer;
pub mod capture;
pub mod emulate;
pub mod backend;
pub mod frontend;
pub mod ioutils;
pub mod scancode;

View File

@@ -1,17 +1,12 @@
use std::{process, error::Error};
use anyhow::Result;
use std::process::{self, Child, Command};
use env_logger::Env;
use lan_mouse::{
consumer, producer,
config::{Config, Frontend::{Cli, Gtk}}, event::server::Server,
frontend::{FrontendAdapter, cli},
};
use lan_mouse::{config::Config, frontend, server::Server};
#[cfg(all(unix, feature = "gtk"))]
use lan_mouse::frontend::gtk;
use tokio::task::LocalSet;
pub fn main() {
// init logging
let env = Env::default().filter_or("LAN_MOUSE_LOG_LEVEL", "info");
env_logger::init_from_env(env);
@@ -22,51 +17,60 @@ pub fn main() {
}
}
pub fn run() -> Result<(), Box<dyn Error>> {
// parse config file
pub fn start_service() -> Result<Child> {
let child = Command::new(std::env::current_exe()?)
.args(std::env::args().skip(1))
.arg("--daemon")
.spawn()?;
Ok(child)
}
pub fn run() -> Result<()> {
// parse config file + cli args
let config = Config::new()?;
log::debug!("{config:?}");
log::info!("release bind: {:?}", config.release_bind);
// start producing and consuming events
let producer = producer::create()?;
let consumer = consumer::create()?;
// create frontend communication adapter
let frontend_adapter = FrontendAdapter::new()?;
// start sending and receiving events
let mut event_server = Server::new(config.port, producer, consumer, frontend_adapter)?;
// any threads need to be started after event_server sets up signal handling
match config.frontend {
#[cfg(all(unix, feature = "gtk"))]
Gtk => { gtk::start()?; }
#[cfg(any(not(feature = "gtk"), not(unix)))]
Gtk => panic!("gtk frontend requested but feature not enabled!"),
Cli => { cli::start()?; }
};
// this currently causes issues, because the clients from
// the config arent communicated to gtk yet.
if config.frontend == Gtk {
log::warn!("clients defined in config currently have no effect with the gtk frontend");
if config.daemon {
// if daemon is specified we run the service
run_service(&config)?;
} else {
// add clients from config
config.get_clients().into_iter().for_each(|(c, h, p)| {
let host_name = match h {
Some(h) => format!(" '{}'", h),
None => "".to_owned(),
};
if c.len() == 0 {
log::warn!("ignoring client{} with 0 assigned ips!", host_name);
// otherwise start the service as a child process and
// run a frontend
let mut service = start_service()?;
frontend::run_frontend(&config)?;
#[cfg(unix)]
{
// on unix we give the service a chance to terminate gracefully
let pid = service.id() as libc::pid_t;
unsafe {
libc::kill(pid, libc::SIGINT);
}
log::info!("adding client [{}]{} @ {:?}", p, host_name, c);
event_server.add_client(c, p);
});
service.wait()?;
}
service.kill()?;
}
log::info!("Press Ctrl+Alt+Shift+Super to release the mouse");
// run event loop
event_server.run()?;
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");
let server = Server::new(config);
server.run().await?;
log::debug!("service exiting");
anyhow::Ok(())
}))?;
Ok(())
}

View File

@@ -1,63 +0,0 @@
use mio::event::Source;
use std::{error::Error, vec::Drain};
use crate::{client::{ClientHandle, ClientEvent}, event::Event};
use crate::backend::producer;
#[cfg(unix)]
use std::env;
#[cfg(unix)]
enum Backend {
Wayland,
X11,
}
pub fn create() -> Result<Box<dyn EventProducer>, Box<dyn Error>> {
#[cfg(windows)]
return Ok(Box::new(producer::windows::WindowsProducer::new()));
#[cfg(unix)]
let backend = match env::var("XDG_SESSION_TYPE") {
Ok(session_type) => match session_type.as_str() {
"x11" => {
log::info!("XDG_SESSION_TYPE = x11 -> using X11 event producer");
Backend::X11
},
"wayland" => {
log::info!("XDG_SESSION_TYPE = wayland -> using wayland event producer");
Backend::Wayland
}
_ => panic!("unknown XDG_SESSION_TYPE"),
},
Err(_) => panic!("could not detect session type: XDG_SESSION_TYPE environment variable not set!"),
};
#[cfg(unix)]
match backend {
Backend::X11 => {
#[cfg(not(feature = "x11"))]
panic!("feature x11 not enabled");
#[cfg(feature = "x11")]
Ok(Box::new(producer::x11::X11Producer::new()))
}
Backend::Wayland => {
#[cfg(not(feature = "wayland"))]
panic!("feature wayland not enabled");
#[cfg(feature = "wayland")]
Ok(Box::new(producer::wayland::WaylandEventProducer::new()?))
}
}
}
pub trait EventProducer: Source {
/// notify event producer of configuration changes
fn notify(&mut self, event: ClientEvent);
/// read an event
/// this function must be invoked to retrieve an Event after
/// the eventfd indicates a pending Event
fn read_events(&mut self) -> Drain<(ClientHandle, Event)>;
/// release mouse
fn release(&mut self);
}

700
src/scancode.rs Normal file
View File

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

206
src/server.rs Normal file
View File

@@ -0,0 +1,206 @@
use log;
use std::{
cell::{Cell, RefCell},
rc::Rc,
};
use tokio::signal;
use crate::{capture, emulate};
use crate::{
client::{ClientHandle, ClientManager},
config::Config,
dns,
frontend::{FrontendEvent, FrontendListener},
server::capture_task::CaptureEvent,
};
use self::{emulation_task::EmulationEvent, resolver_task::DnsRequest};
mod capture_task;
mod emulation_task;
mod frontend_task;
mod network_task;
mod ping_task;
mod resolver_task;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum State {
/// Currently sending events to another device
Sending,
/// Currently receiving events from other devices
Receiving,
/// Entered the deadzone of another device but waiting
/// for acknowledgement (Leave event) from the device
AwaitingLeave,
}
#[derive(Clone)]
pub struct Server {
active_client: Rc<Cell<Option<ClientHandle>>>,
client_manager: Rc<RefCell<ClientManager>>,
port: Rc<Cell<u16>>,
state: Rc<Cell<State>>,
release_bind: Vec<crate::scancode::Linux>,
}
impl Server {
pub fn new(config: &Config) -> Self {
let active_client = Rc::new(Cell::new(None));
let client_manager = Rc::new(RefCell::new(ClientManager::new()));
let state = Rc::new(Cell::new(State::Receiving));
let port = Rc::new(Cell::new(config.port));
for config_client in config.get_clients() {
client_manager.borrow_mut().add_client(
config_client.hostname,
config_client.ips,
config_client.port,
config_client.pos,
config_client.active,
);
}
let release_bind = config.release_bind.clone();
Self {
active_client,
client_manager,
port,
state,
release_bind,
}
}
pub async fn run(&self) -> anyhow::Result<()> {
// create frontend communication adapter
let frontend = match FrontendListener::new().await {
Some(f) => f?,
None => {
// none means some other instance is already running
log::info!("service already running, exiting");
return anyhow::Ok(());
}
};
let (emulate, capture) = tokio::join!(emulate::create(), capture::create());
let (timer_tx, timer_rx) = tokio::sync::mpsc::channel(1);
let (frontend_notify_tx, frontend_notify_rx) = tokio::sync::mpsc::channel(1);
// udp task
let (mut udp_task, sender_tx, receiver_rx, port_tx) =
network_task::new(self.clone(), frontend_notify_tx).await?;
// input capture
let (mut capture_task, capture_channel) = capture_task::new(
capture,
self.clone(),
sender_tx.clone(),
timer_tx.clone(),
self.release_bind.clone(),
);
// input emulation
let (mut emulation_task, emulate_channel) = emulation_task::new(
emulate,
self.clone(),
receiver_rx,
sender_tx.clone(),
capture_channel.clone(),
timer_tx,
);
// create dns resolver
let resolver = dns::DnsResolver::new().await?;
let (mut resolver_task, resolve_tx) = resolver_task::new(resolver, self.clone());
// frontend listener
let (mut frontend_task, frontend_tx) = frontend_task::new(
frontend,
frontend_notify_rx,
self.clone(),
capture_channel.clone(),
emulate_channel.clone(),
resolve_tx.clone(),
port_tx,
);
// task that pings clients to see if they are responding
let mut ping_task = ping_task::new(
self.clone(),
sender_tx.clone(),
emulate_channel.clone(),
capture_channel.clone(),
timer_rx,
);
let active = self
.client_manager
.borrow()
.get_client_states()
.filter_map(|s| {
if s.active {
Some((s.client.handle, s.client.hostname.clone()))
} else {
None
}
})
.collect::<Vec<_>>();
for (handle, hostname) in active {
frontend_tx
.send(FrontendEvent::ActivateClient(handle, true))
.await?;
if let Some(hostname) = hostname {
let _ = resolve_tx.send(DnsRequest { hostname, handle }).await;
}
}
log::info!("running service");
tokio::select! {
_ = signal::ctrl_c() => {
log::info!("terminating service");
}
e = &mut capture_task => {
if let Ok(Err(e)) = e {
log::error!("error in input capture task: {e}");
}
}
e = &mut emulation_task => {
if let Ok(Err(e)) = e {
log::error!("error in input emulation task: {e}");
}
}
e = &mut frontend_task => {
if let Ok(Err(e)) = e {
log::error!("error in frontend listener: {e}");
}
}
_ = &mut resolver_task => { }
_ = &mut udp_task => { }
_ = &mut ping_task => { }
}
let _ = emulate_channel.send(EmulationEvent::Terminate).await;
let _ = capture_channel.send(CaptureEvent::Terminate).await;
let _ = frontend_tx.send(FrontendEvent::Shutdown()).await;
if !capture_task.is_finished() {
if let Err(e) = capture_task.await {
log::error!("error in input capture task: {e}");
}
}
if !emulation_task.is_finished() {
if let Err(e) = emulation_task.await {
log::error!("error in input emulation task: {e}");
}
}
if !frontend_task.is_finished() {
if let Err(e) = frontend_task.await {
log::error!("error in frontend listener: {e}");
}
}
resolver_task.abort();
udp_task.abort();
ping_task.abort();
Ok(())
}
}

152
src/server/capture_task.rs Normal file
View File

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

View File

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

342
src/server/frontend_task.rs Normal file
View File

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

View File

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

131
src/server/ping_task.rs Normal file
View File

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

View File

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