Compare commits

...

59 Commits

Author SHA1 Message Date
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
42 changed files with 3046 additions and 1445 deletions

View File

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

View File

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

View File

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

2
.gitignore vendored
View File

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

711
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.4.0"
version = "0.5.1"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/ferdinandschober/lan-mouse"
@@ -16,46 +16,46 @@ 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"
libc = "0.2.148"
serde_json = "1.0.107"
tokio = {version = "1.32.0", features = ["io-util", "macros", "net", "rt", "sync", "signal"] }
async-trait = "0.1.73"
futures-core = "0.3.28"
futures = "0.3.28"
clap = { version="4.4.11", features = ["derive"] }
gtk = { package = "gtk4", version = "0.7.2", features = ["v4_2"], optional = true }
adw = { package = "libadwaita", version = "0.5.2", features = ["v1_1"], optional = true }
async-channel = { version = "2.1.1", optional = true }
[target.'cfg(unix)'.dependencies]
libc = "0.2.148"
[target.'cfg(all(unix, not(target_os="macos")))'.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 }
wayland-client = { version="0.31.1", optional = true }
wayland-protocols = { version="0.31.0", features=["client", "staging", "unstable"], optional = true }
wayland-protocols-wlr = { version="0.2.0", features=["client"], optional = true }
wayland-protocols-misc = { version="0.2.0", features=["client"], optional = true }
x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
ashpd = { version = "0.6.2", default-features = false, features = ["tokio"], optional = true }
reis = { git = "https://github.com/ids1024/reis", features = [ "tokio" ], optional = true }
[target.'cfg(unix)'.dependencies]
gtk = { package = "gtk4", version = "0.7.2", features = ["v4_6"], optional = true }
adw = { package = "libadwaita", version = "0.5.2", features = ["v1_1"], optional = true }
[target.'cfg(target_os="macos")'.dependencies]
core-graphics = { version = "0.23", features = ["highsierra"] }
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3.9", features = ["winuser"] }
[target.'cfg(unix)'.build-dependencies]
[build-dependencies]
glib-build-tools = "0.18.0"
[features]
default = ["wayland", "x11", "xdg_desktop_portal", "libei", "gtk"]
wayland = ["dep:wayland-client", "dep:wayland-protocols", "dep:wayland-protocols-wlr", "dep:wayland-protocols-misc", "dep:wayland-protocols-plasma"]
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"]
gtk = ["dep:gtk", "dep:adw", "dep:async-channel"]

144
README.md
View File

@@ -1,16 +1,20 @@
# Lan Mouse
- _Now with a gtk frontend_
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 have partial support as well (see below for more details).
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).
![Screenshot from 2023-12-09 01-48-12](https://github.com/feschber/lan-mouse/assets/40996949/016a06a9-76db-4951-9dcc-127d012c59df#gh-dark-mode-only)
![Screenshot from 2023-12-09 01-48-19](https://github.com/feschber/lan-mouse/assets/40996949/d6318340-f811-4e16-9d6e-d1b79883c709#gh-light-mode-only)
- _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, ... .
@@ -18,6 +22,18 @@ Focus lies on performance and a clean, manageable implementation that can easily
For an alternative (with slightly different goals) you may check out [Input Leap](https://github.com/input-leap).
> [!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!
## OS Support
The following table shows support for input emulation (to emulate events received from other clients) and
@@ -28,13 +44,83 @@ input capture (to send events *to* other clients) on different operating systems
| Wayland (wlroots) | :heavy_check_mark: | :heavy_check_mark: |
| Wayland (KDE) | :heavy_check_mark: | :heavy_check_mark: |
| Wayland (Gnome) | :heavy_check_mark: | WIP |
| X11 | (WIP) | WIP |
| Windows | ( :heavy_check_mark: ) | WIP |
| X11 | :heavy_check_mark: | WIP |
| Windows | :heavy_check_mark: | WIP |
| MacOS | ( :heavy_check_mark: ) | WIP |
Keycode translation is not yet implemented so on MacOS only mouse emulation works as of right now.
> [!Important]
> If you are using [Wayfire](https://github.com/WayfireWM/wayfire), make sure to use a recent version (must be newer than October 23rd) and **add `shortcuts-inhibit` to the list of plugins in your wayfire config!**
> Otherwise input capture will not work.
## Build and Run
### Install Dependencies
#### Macos
```sh
brew install libadwaita
```
#### Ubuntu and derivatives
```sh
sudo apt install libadwaita-1-dev libgtk-4-dev libx11-dev libxtst-dev
```
#### Arch and derivatives
```sh
sudo pacman -S libadwaita gtk libx11 libxtst
```
#### Fedora and derivatives
```sh
sudo dnf install libadwaita-devel libXtst-devel libX11-devel
```
#### Windows
Follow the instructions at [gtk-rs.org](https://gtk-rs.org/gtk4-rs/stable/latest/book/installation_windows.html)
*TLDR:*
Build gtk from source
- The following commands should be run in an admin power shell instance:
```sh
# install chocolatey
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
# install python 3.11 (Version is important, as 3.12 does not work currently)
choco install python --version=3.11.0
# install git
choco install git
# install msys2
choco install msys2
# install Visual Studio 2022
choco install visualstudio2022-workload-vctools
```
- The following commands should be run in a regular power shell instance:
```sh
# install gvsbuild with python
python -m pip install --user pipx
python -m pipx ensurepath
pipx install gvsbuild
# build gtk + libadwaita
gvsbuild build gtk4 libadwaita librsvg
```
Make sure to add the directory `C:\gtk-build\gtk\x64\release\bin`
[to the `PATH` environment variable]((https://learn.microsoft.com/en-us/previous-versions/office/developer/sharepoint-2010/ee537574(v=office.14))). Otherwise the project will fail to build.
To avoid building GTK from source, it is possible to disable
the gtk frontend (see conditional compilation below).
### Build and run
Build in release mode:
```sh
cargo build --release
@@ -47,22 +133,23 @@ cargo run --release
### Conditional Compilation
Currently only x11, wayland and windows are supported backends,
Currently only x11, wayland, windows and MacOS are supported backends.
Depending on the toolchain used, support for other platforms is omitted
automatically (it does not make sense to build a Windows `.exe` with
support for x11 and wayland backends).
However one might still want to omit support for e.g. wayland or x11 on
However one might still want to omit support for e.g. wayland, x11 or libei on
a Linux system.
This is possible through
[cargo features](https://doc.rust-lang.org/cargo/reference/features.html)
[cargo features](https://doc.rust-lang.org/cargo/reference/features.html).
E.g. if only wayland support is needed, the following command produces
an executable with just support for wayland:
```sh
cargo build --no-default-features --features wayland
```
For a detailed list of available features, checkout the [Cargo.toml](./Cargo.toml)
## Usage
### Gtk Frontend
@@ -135,22 +222,20 @@ port = 4242
Where `left` can be either `left`, `right`, `top` or `bottom`.
## Roadmap
- [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)
- [ ] Clipboard support
- [x] Graphical frontend (gtk?)
- [ ] *Encryption*
- [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
- [ ] Libei Input Capture
- [ ] X11 Input Capture
- [ ] Windows Input Capture
- [ ] MacOS Input Capture
- [ ] MacOS KeyCode Translation
- [ ] Latency measurement and visualization
- [ ] Bandwidth usage measurement and visualization
- [ ] Clipboard support
- [ ] *Encryption*
## Protocol
Currently *all* mouse and keyboard events are sent via **UDP** for performance reasons.
@@ -222,10 +307,9 @@ 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 (TODO)
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.
#### 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

View File

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

13
lan-mouse.desktop Normal file
View File

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

View File

@@ -14,7 +14,8 @@
<property name="title" translatable="yes">Lan Mouse</property>
<property name="show-menubar">True</property>
<property name="content">
<object class="AdwToolbarView">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child type="top">
<object class="AdwHeaderBar">
<child type ="end">
@@ -28,7 +29,7 @@
</style>
</object>
</child>
<property name="content">
<child>
<object class="AdwToastOverlay" id="toast_overlay">
<child>
<object class="AdwStatusPage">
@@ -138,7 +139,7 @@
</object>
</child>
</object>
</property>
</child>
</object>
</property>
</template>

View File

@@ -15,3 +15,6 @@ pub mod libei;
#[cfg(target_os = "macos")]
pub mod macos;
/// fallback consumer
pub mod dummy;

View File

@@ -0,0 +1,26 @@
use crate::{
client::{ClientEvent, ClientHandle},
consumer::EventConsumer,
event::Event,
};
use async_trait::async_trait;
#[derive(Default)]
pub struct DummyConsumer;
impl DummyConsumer {
pub fn new() -> Self {
Self {}
}
}
#[async_trait]
impl EventConsumer for DummyConsumer {
async fn consume(&mut self, event: Event, client_handle: ClientHandle) {
log::info!("received event: ({client_handle}) {event}");
}
async fn notify(&mut self, client_event: ClientEvent) {
log::info!("{client_event:?}");
}
async fn destroy(&mut self) {}
}

View File

@@ -80,7 +80,7 @@ impl LibeiConsumer {
let context = ei::Context::new(stream)?;
context.flush()?;
let events = EiEventStream::new(context.clone())?;
return Ok(Self {
Ok(Self {
handshake: false,
context,
events,
@@ -96,7 +96,7 @@ impl LibeiConsumer {
capability_mask: 0,
sequence: 0,
serial: 0,
});
})
}
}
@@ -193,7 +193,7 @@ impl EventConsumer for LibeiConsumer {
};
let event = match event {
PendingRequestResult::Request(result) => result,
PendingRequestResult::ProtocolError(e) => {
PendingRequestResult::ParseError(e) => {
return Err(anyhow!("libei protocol violation: {e}"))
}
PendingRequestResult::InvalidObject(e) => return Err(anyhow!("invalid object {e}")),

View File

@@ -3,9 +3,9 @@ use crate::consumer::EventConsumer;
use crate::event::{Event, KeyboardEvent, PointerEvent};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use core_graphics::display::CGPoint;
use core_graphics::display::{CGDisplayBounds, CGMainDisplayID, CGPoint};
use core_graphics::event::{
CGEvent, CGEventTapLocation, CGEventType, CGMouseButton, ScrollEventUnit,
CGEvent, CGEventTapLocation, CGEventType, CGMouseButton, EventField, ScrollEventUnit,
};
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
use std::ops::{Index, IndexMut};
@@ -78,6 +78,16 @@ impl EventConsumer for MacOSConsumer {
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 => {
@@ -85,8 +95,9 @@ impl EventConsumer for MacOSConsumer {
return;
}
};
mouse_location.x += relative_x;
mouse_location.y += relative_y;
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 {
@@ -108,6 +119,14 @@ impl EventConsumer for MacOSConsumer {
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 {
@@ -140,7 +159,7 @@ impl EventConsumer for MacOSConsumer {
}
};
// store button state
self.button_state[mouse_button] = if state == 1 { true } else { false };
self.button_state[mouse_button] = state == 1;
let location = self.get_mouse_location().unwrap();
let event = match CGEvent::new_mouse_event(
@@ -209,9 +228,7 @@ impl EventConsumer for MacOSConsumer {
}
KeyboardEvent::Modifiers { .. } => {}
},
Event::Release() => {}
Event::Ping() => {}
Event::Pong() => {}
_ => (),
}
}

View File

@@ -1,8 +1,13 @@
use crate::{
consumer::EventConsumer,
event::{KeyboardEvent, PointerEvent},
scancode,
};
use anyhow::Result;
use async_trait::async_trait;
use std::time::Duration;
use tokio::task::AbortHandle;
use winapi::um::winuser::{SendInput, KEYEVENTF_EXTENDEDKEY};
use winapi::{
self,
um::winuser::{
@@ -18,11 +23,16 @@ use crate::{
event::Event,
};
pub struct WindowsConsumer {}
const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500);
const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32);
pub struct WindowsConsumer {
repeat_task: Option<AbortHandle>,
}
impl WindowsConsumer {
pub fn new() -> Self {
Self {}
pub fn new() -> Result<Self> {
Ok(Self { repeat_task: None })
}
}
@@ -55,7 +65,15 @@ impl EventConsumer for WindowsConsumer {
time: _,
key,
state,
} => key_event(key, state),
} => {
match state {
// pressed
0 => self.kill_repeat_task(),
1 => self.spawn_repeat_task(key).await,
_ => {}
}
key_event(key, state)
}
KeyboardEvent::Modifiers { .. } => {}
},
_ => {}
@@ -69,6 +87,27 @@ impl EventConsumer for WindowsConsumer {
async fn destroy(&mut self) {}
}
impl WindowsConsumer {
async fn spawn_repeat_task(&mut self, key: u32) {
// there can only be one repeating key and it's
// always the last to be pressed
self.kill_repeat_task();
let repeat_task = tokio::task::spawn_local(async move {
tokio::time::sleep(DEFAULT_REPEAT_DELAY).await;
loop {
key_event(key, 1);
tokio::time::sleep(DEFAULT_REPEAT_INTERVAL).await;
}
});
self.repeat_task = Some(repeat_task.abort_handle());
}
fn kill_repeat_task(&mut self) {
if let Some(task) = self.repeat_task.take() {
task.abort();
}
}
}
fn send_mouse_input(mi: MOUSEINPUT) {
unsafe {
let mut input = INPUT {
@@ -76,8 +115,8 @@ fn send_mouse_input(mi: MOUSEINPUT) {
u: std::mem::transmute(mi),
};
winapi::um::winuser::SendInput(
1 as u32,
SendInput(
1_u32,
&mut input as LPINPUT,
std::mem::size_of::<INPUT>() as i32,
);
@@ -141,10 +180,17 @@ fn scroll(axis: u8, value: f64) {
}
fn key_event(key: u32, state: u8) {
let scancode = match linux_keycode_to_windows_scancode(key) {
Some(code) => code,
None => return,
};
let extended = scancode > 0xff;
let scancode = scancode & 0xff;
let ki = KEYBDINPUT {
wVk: 0,
wScan: key as u16,
wScan: scancode,
dwFlags: KEYEVENTF_SCANCODE
| if extended { KEYEVENTF_EXTENDEDKEY } else { 0 }
| match state {
0 => KEYEVENTF_KEYUP,
1 => 0u32,
@@ -163,6 +209,26 @@ fn send_keyboard_input(ki: KEYBDINPUT) {
u: std::mem::zeroed(),
};
*input.u.ki_mut() = ki;
winapi::um::winuser::SendInput(1 as u32, &mut input, std::mem::size_of::<INPUT>() as i32);
SendInput(1_u32, &mut input, std::mem::size_of::<INPUT>() as i32);
}
}
fn linux_keycode_to_windows_scancode(linux_keycode: u32) -> Option<u16> {
let linux_scancode = match scancode::Linux::try_from(linux_keycode) {
Ok(s) => s,
Err(_) => {
log::warn!("unknown keycode: {linux_keycode}");
return None;
}
};
log::trace!("linux code: {linux_scancode:?}");
let windows_scancode = match scancode::Windows::try_from(linux_scancode) {
Ok(s) => s,
Err(_) => {
log::warn!("failed to translate linux code into windows scancode: {linux_scancode:?}");
return None;
}
};
log::trace!("windows code: {windows_scancode:?}");
Some(windows_scancode as u16)
}

View File

@@ -3,13 +3,11 @@ use crate::consumer::EventConsumer;
use async_trait::async_trait;
use std::collections::HashMap;
use std::io;
use std::os::fd::OwnedFd;
use std::os::unix::prelude::AsRawFd;
use std::os::fd::{AsFd, OwnedFd};
use wayland_client::backend::WaylandError;
use wayland_client::WEnum;
use anyhow::{anyhow, Result};
use wayland_client::globals::BindError;
use wayland_client::protocol::wl_keyboard::{self, WlKeyboard};
use wayland_client::protocol::wl_pointer::{Axis, ButtonState};
use wayland_client::protocol::wl_seat::WlSeat;
@@ -23,8 +21,6 @@ use wayland_protocols_misc::zwp_virtual_keyboard_v1::client::{
zwp_virtual_keyboard_v1::ZwpVirtualKeyboardV1 as Vk,
};
use wayland_protocols_plasma::fake_input::client::org_kde_kwin_fake_input::OrgKdeKwinFakeInput;
use wayland_client::{
delegate_noop,
globals::{registry_queue_init, GlobalListContents},
@@ -34,17 +30,13 @@ use wayland_client::{
use crate::event::{Event, KeyboardEvent, PointerEvent};
enum VirtualInputManager {
Wlroots { vpm: VpManager, vkm: VkManager },
Kde { fake_input: OrgKdeKwinFakeInput },
}
struct State {
keymap: Option<(u32, OwnedFd, u32)>,
input_for_client: HashMap<ClientHandle, VirtualInput>,
seat: wl_seat::WlSeat,
virtual_input_manager: VirtualInputManager,
qh: QueueHandle<Self>,
vpm: VpManager,
vkm: VkManager,
}
// App State, implements Dispatch event handlers
@@ -56,8 +48,8 @@ pub(crate) struct WlrootsConsumer {
impl WlrootsConsumer {
pub fn new() -> Result<Self> {
let conn = Connection::connect_to_env().unwrap();
let (globals, queue) = registry_queue_init::<State>(&conn).unwrap();
let conn = Connection::connect_to_env()?;
let (globals, queue) = registry_queue_init::<State>(&conn)?;
let qh = queue.handle();
let seat: wl_seat::WlSeat = match globals.bind(&qh, 7..=8, ()) {
@@ -65,30 +57,8 @@ impl WlrootsConsumer {
Err(_) => return Err(anyhow!("wl_seat >= v7 not supported")),
};
let vpm: Result<VpManager, BindError> = globals.bind(&qh, 1..=1, ());
let vkm: Result<VkManager, BindError> = globals.bind(&qh, 1..=1, ());
let fake_input: Result<OrgKdeKwinFakeInput, BindError> = globals.bind(&qh, 4..=4, ());
let virtual_input_manager = match (vpm, vkm, fake_input) {
(Ok(vpm), Ok(vkm), _) => VirtualInputManager::Wlroots { vpm, vkm },
(_, _, Ok(fake_input)) => {
fake_input.authenticate(
"lan-mouse".into(),
"Allow remote clients to control this device".into(),
);
VirtualInputManager::Kde { fake_input }
}
(Err(e1), Err(e2), Err(e3)) => {
log::warn!("zwlr_virtual_pointer_v1: {e1}");
log::warn!("zwp_virtual_keyboard_v1: {e2}");
log::warn!("org_kde_kwin_fake_input: {e3}");
log::error!("neither wlroots nor kde input emulation protocol supported!");
return Err(anyhow!("could not create event consumer"));
}
_ => {
panic!()
}
};
let vpm: VpManager = globals.bind(&qh, 1..=1, ())?;
let vkm: VkManager = globals.bind(&qh, 1..=1, ())?;
let input_for_client: HashMap<ClientHandle, VirtualInput> = HashMap::new();
@@ -98,7 +68,8 @@ impl WlrootsConsumer {
keymap: None,
input_for_client,
seat,
virtual_input_manager,
vpm,
vkm,
qh,
},
queue,
@@ -118,29 +89,19 @@ impl WlrootsConsumer {
impl State {
fn add_client(&mut self, client: ClientHandle) {
// create virtual input devices
match &self.virtual_input_manager {
VirtualInputManager::Wlroots { vpm, vkm } => {
let pointer: Vp = vpm.create_virtual_pointer(None, &self.qh, ());
let keyboard: Vk = vkm.create_virtual_keyboard(&self.seat, &self.qh, ());
let pointer: Vp = self.vpm.create_virtual_pointer(None, &self.qh, ());
let keyboard: Vk = self.vkm.create_virtual_keyboard(&self.seat, &self.qh, ());
// TODO: use server side keymap
if let Some((format, fd, size)) = self.keymap.as_ref() {
keyboard.keymap(*format, fd.as_raw_fd(), *size);
} else {
panic!("no keymap");
}
let vinput = VirtualInput::Wlroots { pointer, keyboard };
self.input_for_client.insert(client, vinput);
}
VirtualInputManager::Kde { fake_input } => {
let fake_input = fake_input.clone();
let vinput = VirtualInput::Kde { fake_input };
self.input_for_client.insert(client, vinput);
}
// TODO: use server side keymap
if let Some((format, fd, size)) = self.keymap.as_ref() {
keyboard.keymap(*format, fd.as_fd(), *size);
} else {
panic!("no keymap");
}
let vinput = VirtualInput { pointer, keyboard };
self.input_for_client.insert(client, vinput);
}
}
@@ -194,9 +155,9 @@ impl EventConsumer for WlrootsConsumer {
async fn destroy(&mut self) {}
}
enum VirtualInput {
Wlroots { pointer: Vp, keyboard: Vk },
Kde { fake_input: OrgKdeKwinFakeInput },
struct VirtualInput {
pointer: Vp,
keyboard: Vk,
}
impl VirtualInput {
@@ -208,94 +169,37 @@ impl VirtualInput {
time,
relative_x,
relative_y,
} => match self {
VirtualInput::Wlroots {
pointer,
keyboard: _,
} => {
pointer.motion(time, relative_x, relative_y);
}
VirtualInput::Kde { fake_input } => {
fake_input.pointer_motion(relative_y, relative_y);
}
},
} => self.pointer.motion(time, relative_x, relative_y),
PointerEvent::Button {
time,
button,
state,
} => {
let state: ButtonState = state.try_into()?;
match self {
VirtualInput::Wlroots {
pointer,
keyboard: _,
} => {
pointer.button(time, button, state);
}
VirtualInput::Kde { fake_input } => {
fake_input.button(button, state as u32);
}
}
self.pointer.button(time, button, state);
}
PointerEvent::Axis { time, axis, value } => {
let axis: Axis = (axis as u32).try_into()?;
match self {
VirtualInput::Wlroots {
pointer,
keyboard: _,
} => {
pointer.axis(time, axis, value);
pointer.frame();
}
VirtualInput::Kde { fake_input } => {
fake_input.axis(axis as u32, value);
}
}
self.pointer.axis(time, axis, value);
self.pointer.frame();
}
PointerEvent::Frame {} => match self {
VirtualInput::Wlroots {
pointer,
keyboard: _,
} => {
pointer.frame();
}
VirtualInput::Kde { fake_input: _ } => {}
},
}
match self {
VirtualInput::Wlroots { pointer, .. } => {
// insert a frame event after each mouse event
pointer.frame();
}
_ => {}
PointerEvent::Frame {} => self.pointer.frame(),
}
self.pointer.frame();
}
Event::Keyboard(e) => match e {
KeyboardEvent::Key { time, key, state } => match self {
VirtualInput::Wlroots {
pointer: _,
keyboard,
} => {
keyboard.key(time, key, state as u32);
}
VirtualInput::Kde { fake_input } => {
fake_input.keyboard_key(key, state as u32);
}
},
KeyboardEvent::Key { time, key, state } => {
self.keyboard.key(time, key, state as u32);
}
KeyboardEvent::Modifiers {
mods_depressed,
mods_latched,
mods_locked,
group,
} => match self {
VirtualInput::Wlroots {
pointer: _,
keyboard,
} => {
keyboard.modifiers(mods_depressed, mods_latched, mods_locked, group);
}
VirtualInput::Kde { fake_input: _ } => {}
},
} => {
self.keyboard
.modifiers(mods_depressed, mods_latched, mods_locked, group);
}
},
_ => {}
}
@@ -307,7 +211,6 @@ delegate_noop!(State: Vp);
delegate_noop!(State: Vk);
delegate_noop!(State: VpManager);
delegate_noop!(State: VkManager);
delegate_noop!(State: OrgKdeKwinFakeInput);
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for State {
fn event(
@@ -330,11 +233,8 @@ impl Dispatch<WlKeyboard, ()> for State {
_: &Connection,
_: &QueueHandle<Self>,
) {
match event {
wl_keyboard::Event::Keymap { format, fd, size } => {
state.keymap = Some((u32::from(format), fd, size));
}
_ => {}
if let wl_keyboard::Event::Keymap { format, fd, size } = event {
state.keymap = Some((u32::from(format), fd, size));
}
}
}

View File

@@ -1,8 +1,18 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use std::ptr;
use x11::{xlib, xtest};
use x11::{
xlib::{self, XCloseDisplay},
xtest,
};
use crate::{client::ClientHandle, consumer::EventConsumer, event::Event};
use crate::{
client::ClientHandle,
consumer::EventConsumer,
event::{
Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT,
},
};
pub struct X11Consumer {
display: *mut xlib::Display,
@@ -11,21 +21,80 @@ pub struct X11Consumer {
unsafe impl Send for X11Consumer {}
impl X11Consumer {
pub fn new() -> Self {
pub fn new() -> Result<Self> {
let display = unsafe {
match xlib::XOpenDisplay(ptr::null()) {
d if d == ptr::null::<xlib::Display>() as *mut xlib::Display => None,
display => Some(display),
d if d == ptr::null::<xlib::Display>() as *mut xlib::Display => {
Err(anyhow!("could not open display"))
}
display => Ok(display),
}
};
let display = display.expect("could not open display");
Self { display }
}?;
Ok(Self { display })
}
fn relative_motion(&self, dx: i32, dy: i32) {
unsafe {
xtest::XTestFakeRelativeMotionEvent(self.display, dx, dy, 0, 0);
xlib::XFlush(self.display);
}
}
fn emulate_mouse_button(&self, button: u32, state: u32) {
unsafe {
let x11_button = match button {
BTN_RIGHT => 3,
BTN_MIDDLE => 2,
BTN_BACK => 8,
BTN_FORWARD => 9,
BTN_LEFT => 1,
_ => 1,
};
xtest::XTestFakeButtonEvent(self.display, x11_button, state as i32, 0);
};
}
const SCROLL_UP: u32 = 4;
const SCROLL_DOWN: u32 = 5;
const SCROLL_LEFT: u32 = 6;
const SCROLL_RIGHT: u32 = 7;
fn emulate_scroll(&self, axis: u8, value: f64) {
let direction = match axis {
1 => {
if value < 0.0 {
Self::SCROLL_LEFT
} else {
Self::SCROLL_RIGHT
}
}
_ => {
if value < 0.0 {
Self::SCROLL_UP
} else {
Self::SCROLL_DOWN
}
}
};
unsafe {
xtest::XTestFakeButtonEvent(self.display, direction, 1, 0);
xtest::XTestFakeButtonEvent(self.display, direction, 0, 0);
}
}
#[allow(dead_code)]
fn emulate_key(&self, key: u32, state: u8) {
let key = key + 8; // xorg keycodes are shifted by 8
unsafe {
xtest::XTestFakeKeyEvent(self.display, key, state as i32, 0);
}
}
}
impl Drop for X11Consumer {
fn drop(&mut self) {
unsafe {
XCloseDisplay(self.display);
}
}
}
@@ -35,20 +104,41 @@ impl EventConsumer for X11Consumer {
async fn consume(&mut self, event: Event, _: ClientHandle) {
match event {
Event::Pointer(pointer_event) => match pointer_event {
crate::event::PointerEvent::Motion {
PointerEvent::Motion {
time: _,
relative_x,
relative_y,
} => {
self.relative_motion(relative_x as i32, relative_y as i32);
}
crate::event::PointerEvent::Button { .. } => {}
crate::event::PointerEvent::Axis { .. } => {}
crate::event::PointerEvent::Frame {} => {}
PointerEvent::Button {
time: _,
button,
state,
} => {
self.emulate_mouse_button(button, state);
}
PointerEvent::Axis {
time: _,
axis,
value,
} => {
self.emulate_scroll(axis, value);
}
PointerEvent::Frame {} => {}
},
Event::Keyboard(_) => {}
Event::Keyboard(KeyboardEvent::Key {
time: _,
key,
state,
}) => {
self.emulate_key(key, state);
}
_ => {}
}
unsafe {
xlib::XFlush(self.display);
}
}
async fn notify(&mut self, _: crate::client::ClientEvent) {

View File

@@ -8,7 +8,14 @@ use ashpd::{
};
use async_trait::async_trait;
use crate::consumer::EventConsumer;
use crate::{
client::ClientEvent,
consumer::EventConsumer,
event::{
Event::{Keyboard, Pointer},
KeyboardEvent, PointerEvent,
},
};
pub struct DesktopPortalConsumer<'a> {
proxy: RemoteDesktop<'a>,
@@ -17,8 +24,11 @@ pub struct DesktopPortalConsumer<'a> {
impl<'a> DesktopPortalConsumer<'a> {
pub async fn new() -> Result<DesktopPortalConsumer<'a>> {
log::debug!("connecting to org.freedesktop.portal.RemoteDesktop portal ...");
let proxy = RemoteDesktop::new().await?;
log::debug!("creating session ...");
let session = proxy.create_session().await?;
log::debug!("selecting devices ...");
proxy
.select_devices(&session, DeviceType::Keyboard | DeviceType::Pointer)
.await?;
@@ -27,6 +37,7 @@ impl<'a> DesktopPortalConsumer<'a> {
.start(&session, &WindowIdentifier::default())
.await?
.response()?;
log::debug!("started session");
Ok(Self { proxy, session })
}
@@ -36,9 +47,9 @@ impl<'a> DesktopPortalConsumer<'a> {
impl<'a> EventConsumer for DesktopPortalConsumer<'a> {
async fn consume(&mut self, event: crate::event::Event, _client: crate::client::ClientHandle) {
match event {
crate::event::Event::Pointer(p) => {
Pointer(p) => {
match p {
crate::event::PointerEvent::Motion {
PointerEvent::Motion {
time: _,
relative_x,
relative_y,
@@ -51,7 +62,7 @@ impl<'a> EventConsumer for DesktopPortalConsumer<'a> {
log::warn!("{e}");
}
}
crate::event::PointerEvent::Button {
PointerEvent::Button {
time: _,
button,
state,
@@ -68,7 +79,7 @@ impl<'a> EventConsumer for DesktopPortalConsumer<'a> {
log::warn!("{e}");
}
}
crate::event::PointerEvent::Axis {
PointerEvent::Axis {
time: _,
axis,
value,
@@ -86,12 +97,12 @@ impl<'a> EventConsumer for DesktopPortalConsumer<'a> {
log::warn!("{e}");
}
}
crate::event::PointerEvent::Frame {} => {}
PointerEvent::Frame {} => {}
}
}
crate::event::Event::Keyboard(k) => {
Keyboard(k) => {
match k {
crate::event::KeyboardEvent::Key {
KeyboardEvent::Key {
time: _,
key,
state,
@@ -108,7 +119,7 @@ impl<'a> EventConsumer for DesktopPortalConsumer<'a> {
log::warn!("{e}");
}
}
crate::event::KeyboardEvent::Modifiers { .. } => {
KeyboardEvent::Modifiers { .. } => {
// ignore
}
}
@@ -117,7 +128,7 @@ impl<'a> EventConsumer for DesktopPortalConsumer<'a> {
}
}
async fn notify(&mut self, _client: crate::client::ClientEvent) {}
async fn notify(&mut self, _client: ClientEvent) {}
async fn destroy(&mut self) {
log::debug!("closing remote desktop session");

View File

@@ -1,10 +1,17 @@
#[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 event producer
pub mod dummy;

View File

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

View File

@@ -1,22 +1,30 @@
use anyhow::Result;
use anyhow::{anyhow, Result};
use std::{io, task::Poll};
use futures_core::Stream;
use crate::{client::ClientHandle, event::Event, producer::EventProducer};
use crate::{
client::{ClientEvent, ClientHandle},
event::Event,
producer::EventProducer,
};
pub struct LibeiProducer {}
impl LibeiProducer {
pub fn new() -> Result<Self> {
Ok(Self {})
Err(anyhow!("not implemented"))
}
}
impl EventProducer for LibeiProducer {
fn notify(&mut self, _event: crate::client::ClientEvent) {}
fn notify(&mut self, _event: ClientEvent) -> io::Result<()> {
Ok(())
}
fn release(&mut self) {}
fn release(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Stream for LibeiProducer {

View File

@@ -1,6 +1,7 @@
use crate::client::{ClientEvent, ClientHandle};
use crate::event::Event;
use crate::producer::EventProducer;
use anyhow::{anyhow, Result};
use futures_core::Stream;
use std::task::{Context, Poll};
use std::{io, pin::Pin};
@@ -8,8 +9,8 @@ use std::{io, pin::Pin};
pub struct MacOSProducer;
impl MacOSProducer {
pub fn new() -> Self {
Self {}
pub fn new() -> Result<Self> {
Err(anyhow!("not yet implemented"))
}
}
@@ -22,7 +23,11 @@ impl Stream for MacOSProducer {
}
impl EventProducer for MacOSProducer {
fn notify(&mut self, _event: ClientEvent) {}
fn notify(&mut self, _event: ClientEvent) -> io::Result<()> {
Ok(())
}
fn release(&mut self) {}
fn release(&mut self) -> io::Result<()> {
Ok(())
}
}

View File

@@ -10,7 +10,7 @@ use std::{
collections::VecDeque,
env,
io::{self, ErrorKind},
os::fd::{OwnedFd, RawFd},
os::fd::{AsFd, OwnedFd, RawFd},
pin::Pin,
task::{ready, Context, Poll},
};
@@ -125,6 +125,7 @@ struct Window {
buffer: wl_buffer::WlBuffer,
surface: wl_surface::WlSurface,
layer_surface: ZwlrLayerSurfaceV1,
pos: Position,
}
impl Window {
@@ -145,7 +146,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,
@@ -159,7 +160,7 @@ impl Window {
let layer_surface = g.layer_shell.get_layer_surface(
&surface,
Some(&output),
Some(output),
Layer::Overlay,
"LAN Mouse Sharing".into(),
qh,
@@ -179,6 +180,7 @@ impl Window {
surface.set_input_region(None);
surface.commit();
Window {
pos,
buffer,
surface,
layer_surface,
@@ -221,13 +223,7 @@ fn get_output_configuration(state: &State, pos: Position) -> Vec<(WlOutput, Outp
// 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()
})
.filter(|(_, edge)| !opposite_edges.iter().map(|(_, e)| *e).any(|e| &e == edge))
.map(|(o, _)| o.clone())
.collect();
state
@@ -244,7 +240,7 @@ fn draw(f: &mut File, (width, height): (u32, u32)) {
for _ in 0..width {
if env::var("LM_DEBUG_LAYER_SHELL").ok().is_some() {
// AARRGGBB
buf.write_all(&0xFF11d116u32.to_ne_bytes()).unwrap();
buf.write_all(&0xff11d116u32.to_ne_bytes()).unwrap();
} else {
// AARRGGBB
buf.write_all(&0x00000000u32.to_ne_bytes()).unwrap();
@@ -330,7 +326,7 @@ impl WaylandEventProducer {
queue.flush()?;
// prepare reading wayland events
let read_guard = queue.prepare_read()?;
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);
@@ -372,13 +368,39 @@ 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);
let inner = AsyncFd::new(Inner { queue, state })?;
Ok(WaylandEventProducer(inner))
}
fn add_client(&mut self, handle: ClientHandle, pos: Position) {
self.0.get_mut().state.add_client(handle, pos);
}
fn delete_client(&mut self, handle: ClientHandle) {
let inner = self.0.get_mut();
// remove all windows corresponding to this client
while let Some(i) = inner
.state
.client_for_window
.iter()
.position(|(_, c)| *c == handle)
{
inner.state.client_for_window.remove(i);
inner.state.focused = None;
}
}
}
impl State {
@@ -468,11 +490,24 @@ impl State {
let outputs = get_output_configuration(self, pos);
outputs.iter().for_each(|(o, i)| {
let window = Window::new(&self, &self.qh, &o, pos, i.size);
let window = 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 Inner {
@@ -490,16 +525,20 @@ impl Inner {
}
}
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) {
@@ -521,54 +560,42 @@ impl Inner {
}
}
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 notify(&mut self, client_event: ClientEvent) {
fn notify(&mut self, client_event: ClientEvent) -> io::Result<()> {
match client_event {
ClientEvent::Create(handle, pos) => {
self.0.get_mut().state.add_client(handle, pos);
self.add_client(handle, pos);
}
ClientEvent::Destroy(handle) => {
let inner = self.0.get_mut();
loop {
// remove all windows corresponding to this client
if let Some(i) = inner
.state
.client_for_window
.iter()
.position(|(_, c)| *c == handle)
{
inner.state.client_for_window.remove(i);
inner.state.focused = None;
} else {
break;
}
}
self.delete_client(handle);
}
}
let inner = self.0.get_mut();
inner.flush_events();
inner.flush_events()
}
fn release(&mut self) {
fn release(&mut self) -> io::Result<()> {
log::debug!("releasing pointer");
let inner = self.0.get_mut();
inner.state.ungrab();
inner.flush_events();
inner.flush_events()
}
}
@@ -576,7 +603,6 @@ impl Stream for WaylandEventProducer {
type Item = io::Result<(ClientHandle, Event)>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
log::trace!("producer.next()");
if let Some(event) = self.0.get_mut().state.pending_events.pop_front() {
return Poll::Ready(Some(Ok(event)));
}
@@ -590,17 +616,27 @@ impl Stream for WaylandEventProducer {
// read events
while inner.read() {
// prepare next read
inner.prepare_read();
match inner.prepare_read() {
Ok(_) => {}
Err(e) => return Poll::Ready(Some(Err(e))),
}
}
// dispatch the events
inner.dispatch_events();
// flush outgoing events
inner.flush_events();
if let Err(e) = inner.flush_events() {
if e.kind() != ErrorKind::WouldBlock {
return Poll::Ready(Some(Err(e)));
}
}
// prepare for the next read
inner.prepare_read();
match inner.prepare_read() {
Ok(_) => {}
Err(e) => return Poll::Ready(Some(Err(e))),
}
}
// clear read readiness for tokio read guard
@@ -663,7 +699,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
.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;
}
@@ -673,9 +709,16 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
.iter()
.find(|(w, _c)| w.surface == surface)
.unwrap();
app.pending_events.push_back((*client, Event::Release()));
app.pending_events.push_back((*client, Event::Enter()));
}
wl_pointer::Event::Leave { .. } => {
/* There are rare cases, where when a window is opened in
* just the wrong moment, the pointer is released, while
* still grabbed.
* In that case, the pointer must be ungrabbed, otherwise
* it is impossible to grab it again (since the pointer
* lock, relative pointer,... objects are still in place)
*/
app.ungrab();
}
wl_pointer::Event::Button {
@@ -764,10 +807,6 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for State {
}),
));
}
if mods_depressed == 77 {
// ctrl shift super alt
app.ungrab();
}
}
wl_keyboard::Event::Keymap {
format: _,
@@ -834,7 +873,7 @@ impl Dispatch<ZwlrLayerSurfaceV1, ()> for State {
// 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();
}
@@ -869,16 +908,15 @@ impl Dispatch<wl_registry::WlRegistry, ()> for State {
name,
interface,
version: _,
} => match interface.as_str() {
"wl_output" => {
} => {
if interface.as_str() == "wl_output" {
log::debug!("wl_output global");
state
.g
.outputs
.push(registry.bind::<wl_output::WlOutput, _, _>(name, 4, qh, ()))
}
_ => {}
},
}
wl_registry::Event::GlobalRemove { .. } => {}
_ => {}
}
@@ -921,6 +959,21 @@ impl Dispatch<ZxdgOutputV1, WlOutput> for State {
}
}
impl Dispatch<wl_output::WlOutput, ()> for State {
fn event(
state: &mut Self,
_proxy: &wl_output::WlOutput,
event: <wl_output::WlOutput as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
if let wl_output::Event::Done = event {
state.update_windows();
}
}
}
// don't emit any events
delegate_noop!(State: wl_region::WlRegion);
delegate_noop!(State: wl_shm_pool::WlShmPool);
@@ -931,7 +984,6 @@ 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);

View File

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

View File

@@ -1,3 +1,4 @@
use anyhow::{anyhow, Result};
use std::io;
use std::task::Poll;
@@ -11,15 +12,19 @@ use crate::client::{ClientEvent, ClientHandle};
pub struct X11Producer {}
impl X11Producer {
pub fn new() -> Self {
Self {}
pub fn new() -> Result<Self> {
Err(anyhow!("not implemented"))
}
}
impl EventProducer for X11Producer {
fn notify(&mut self, _: ClientEvent) {}
fn notify(&mut self, _event: ClientEvent) -> io::Result<()> {
Ok(())
}
fn release(&mut self) {}
fn release(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Stream for X11Producer {

View File

@@ -2,7 +2,6 @@ use std::{
collections::HashSet,
fmt::Display,
net::{IpAddr, SocketAddr},
time::Instant,
};
use serde::{Deserialize, Serialize};
@@ -51,14 +50,12 @@ impl Display for Position {
pub struct Client {
/// hostname of this client
pub hostname: Option<String>,
/// fix ips, determined by the user
pub fix_ips: Vec<IpAddr>,
/// unique handle to refer to the client.
/// This way any event consumer / producer backend does not
/// need to know anything about a client other than its handle.
pub handle: ClientHandle,
/// `active` address of the client, used to send data to.
/// This should generally be the socket address where data
/// was last received from.
pub active_addr: Option<SocketAddr>,
/// all socket addresses associated with a particular client
/// e.g. Laptops usually have at least an ethernet and a wifi port
/// which have different ip addresses
@@ -69,6 +66,7 @@ pub struct Client {
pub pos: Position,
}
#[derive(Clone, Copy, Debug)]
pub enum ClientEvent {
Create(ClientHandle, Position),
Destroy(ClientHandle),
@@ -78,17 +76,30 @@ pub type ClientHandle = u32;
#[derive(Debug, Clone)]
pub struct ClientState {
/// information about the client
pub client: Client,
/// events should be sent to and received from the client
pub active: bool,
pub last_ping: Option<Instant>,
pub last_seen: Option<Instant>,
pub last_replied: Option<Instant>,
/// `active` address of the client, used to send data to.
/// This should generally be the socket address where data
/// was last received from.
pub active_addr: Option<SocketAddr>,
/// tracks whether or not the client is responding to pings
pub alive: bool,
/// keys currently pressed by this client
pub pressed_keys: HashSet<u32>,
}
pub struct ClientManager {
clients: Vec<Option<ClientState>>, // HashMap likely not beneficial
}
impl Default for ClientManager {
fn default() -> Self {
Self::new()
}
}
impl ClientManager {
pub fn new() -> Self {
Self { clients: vec![] }
@@ -98,24 +109,24 @@ impl ClientManager {
pub fn add_client(
&mut self,
hostname: Option<String>,
addrs: HashSet<IpAddr>,
ips: HashSet<IpAddr>,
port: u16,
pos: Position,
) -> ClientHandle {
// get a new client_handle
let handle = self.free_id();
// we dont know, which IP is initially active
let active_addr = None;
// store fix ip addresses
let fix_ips = ips.iter().cloned().collect();
// map ip addresses to socket addresses
let addrs = HashSet::from_iter(addrs.into_iter().map(|ip| SocketAddr::new(ip, port)));
let addrs = HashSet::from_iter(ips.into_iter().map(|ip| SocketAddr::new(ip, port)));
// store the client
let client = Client {
hostname,
fix_ips,
handle,
active_addr,
addrs,
port,
pos,
@@ -124,10 +135,10 @@ impl ClientManager {
// client was never seen, nor pinged
let client_state = ClientState {
client,
last_ping: None,
last_seen: None,
last_replied: None,
active: false,
active_addr: None,
alive: false,
pressed_keys: HashSet::new(),
};
if handle as usize >= self.clients.len() {
@@ -174,20 +185,20 @@ impl ClientManager {
}
// returns an immutable reference to the client state corresponding to `client`
pub fn get<'a>(&'a self, client: ClientHandle) -> Option<&'a ClientState> {
pub fn get(&self, client: ClientHandle) -> Option<&ClientState> {
self.clients.get(client as usize)?.as_ref()
}
/// returns a mutable reference to the client state corresponding to `client`
pub fn get_mut<'a>(&'a mut self, client: ClientHandle) -> Option<&'a mut ClientState> {
pub fn get_mut(&mut self, client: ClientHandle) -> Option<&mut ClientState> {
self.clients.get_mut(client as usize)?.as_mut()
}
pub fn enumerate(&self) -> Vec<(Client, bool)> {
self.clients
.iter()
.filter_map(|s| s.as_ref())
.map(|s| (s.client.clone(), s.active))
.collect()
pub fn get_client_states(&self) -> impl Iterator<Item = &ClientState> {
self.clients.iter().filter_map(|x| x.as_ref())
}
pub fn get_client_states_mut(&mut self) -> impl Iterator<Item = &mut ClientState> {
self.clients.iter_mut().filter_map(|x| x.as_mut())
}
}

View File

@@ -109,9 +109,9 @@ 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,

View File

@@ -1,9 +1,6 @@
use async_trait::async_trait;
use std::future;
#[cfg(all(unix, not(target_os = "macos")))]
use std::env;
use crate::{
backend::consumer,
client::{ClientEvent, ClientHandle},
@@ -11,15 +8,6 @@ use crate::{
};
use anyhow::Result;
#[cfg(all(unix, not(target_os = "macos")))]
#[derive(Debug)]
enum Backend {
Wlroots,
X11,
RemoteDesktopPortal,
Libei,
}
#[async_trait]
pub trait EventConsumer: Send {
async fn consume(&mut self, event: Event, client_handle: ClientHandle);
@@ -33,90 +21,58 @@ pub trait EventConsumer: Send {
async fn destroy(&mut self);
}
pub async fn create() -> Result<Box<dyn EventConsumer>> {
pub async fn create() -> Box<dyn EventConsumer> {
#[cfg(windows)]
return Ok(Box::new(consumer::windows::WindowsConsumer::new()));
match consumer::windows::WindowsConsumer::new() {
Ok(c) => return Box::new(c),
Err(e) => log::warn!("windows event consumer unavailable: {e}"),
}
#[cfg(target_os = "macos")]
return Ok(Box::new(consumer::macos::MacOSConsumer::new()?));
#[cfg(all(unix, not(target_os = "macos")))]
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(all(unix, not(target_os = "macos")))]
match backend {
Backend::Libei => {
#[cfg(not(feature = "libei"))]
panic!("feature libei not enabled");
#[cfg(feature = "libei")]
Ok(Box::new(consumer::libei::LibeiConsumer::new().await?))
}
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().await?,
))
}
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()))
match consumer::macos::MacOSConsumer::new() {
Ok(c) => {
log::info!("using macos event consumer");
return Box::new(c);
}
Err(e) => log::error!("macos consumer not available: {e}"),
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
match consumer::wlroots::WlrootsConsumer::new() {
Ok(c) => {
log::info!("using wlroots event consumer");
return Box::new(c);
}
Err(e) => log::info!("wayland backend not available: {e}"),
}
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
match consumer::libei::LibeiConsumer::new().await {
Ok(c) => {
log::info!("using libei event consumer");
return Box::new(c);
}
Err(e) => log::info!("libei not available: {e}"),
}
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
match consumer::xdg_desktop_portal::DesktopPortalConsumer::new().await {
Ok(c) => {
log::info!("using xdg-remote-desktop-portal event consumer");
return Box::new(c);
}
Err(e) => log::info!("remote desktop portal not available: {e}"),
}
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
match consumer::x11::X11Consumer::new() {
Ok(c) => {
log::info!("using x11 event consumer");
return Box::new(c);
}
Err(e) => log::info!("x11 consumer not available: {e}"),
}
log::error!("falling back to dummy event consumer");
Box::new(consumer::dummy::DummyConsumer::new())
}

View File

@@ -3,7 +3,7 @@ use std::{error::Error, net::IpAddr};
use trust_dns_resolver::TokioAsyncResolver;
pub(crate) struct DnsResolver {
pub struct DnsResolver {
resolver: TokioAsyncResolver,
}
impl DnsResolver {

View File

@@ -1,14 +1,17 @@
use anyhow::{anyhow, Result};
use std::{
error::Error,
fmt::{self, Display},
};
// FIXME
pub(crate) const BTN_LEFT: u32 = 0x110;
pub(crate) const BTN_RIGHT: u32 = 0x111;
pub(crate) const BTN_MIDDLE: u32 = 0x112;
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,
@@ -28,7 +31,7 @@ pub enum PointerEvent {
Frame {},
}
#[derive(Debug, Clone, Copy)]
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum KeyboardEvent {
Key {
time: u32,
@@ -43,13 +46,29 @@ 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 {
@@ -101,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"),
}
}
}
@@ -111,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,
}
}
}
@@ -123,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,
}
}
}
@@ -134,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),
})),
}
@@ -175,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()
}
}
@@ -215,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,
@@ -244,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,
@@ -254,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() {
@@ -281,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(),
}))
}
@@ -293,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(),
}))
}
@@ -301,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(),
}))
}
@@ -312,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(),
}))
}
@@ -324,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(),
}))
}
@@ -332,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(),
}))
}
@@ -343,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(),
}))
}
@@ -355,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(),
}));
}
@@ -363,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,
@@ -400,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[..],
@@ -409,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() {
@@ -424,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(),
}))
}
@@ -436,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(),
}))
}
@@ -444,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(),
}))
}
@@ -463,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(),
}))
}
@@ -471,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(),
}))
}
@@ -479,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(),
}))
}
@@ -493,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

@@ -31,16 +31,16 @@ use crate::{
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(all(unix, feature = "gtk"))]
#[cfg(feature = "gtk")]
Frontend::Gtk => {
gtk::run();
}
#[cfg(any(not(feature = "gtk"), not(unix)))]
#[cfg(not(feature = "gtk"))]
Frontend::Gtk => panic!("gtk frontend requested but feature not enabled!"),
Frontend::Cli => {
cli::run()?;
@@ -108,6 +108,7 @@ pub enum FrontendNotify {
NotifyClientDelete(ClientHandle),
/// new port, reason of failure (if failed)
NotifyPortChange(u16, Option<String>),
/// Client State, active
Enumerate(Vec<(Client, bool)>),
NotifyError(String),
}
@@ -201,8 +202,6 @@ impl FrontendListener {
#[cfg(unix)]
pub async fn accept(&mut self) -> Result<ReadHalf<UnixStream>> {
log::trace!("frontend.accept()");
let stream = self.listener.accept().await?.0;
let (rx, tx) = tokio::io::split(stream);
self.tx_streams.push(tx);
@@ -229,11 +228,11 @@ impl FrontendListener {
// TODO do simultaneously
for tx in self.tx_streams.iter_mut() {
// write len + payload
if let Err(_) = tx.write(&len).await {
if tx.write(&len).await.is_err() {
keep.push(false);
continue;
}
if let Err(_) = tx.write(payload).await {
if tx.write(payload).await.is_err() {
keep.push(false);
continue;
}

View File

@@ -1,32 +1,17 @@
use anyhow::{anyhow, Context, Result};
#[cfg(windows)]
use std::net::SocketAddrV4;
use std::{
io::{ErrorKind, Read, Write},
str::SplitWhitespace,
thread,
};
#[cfg(windows)]
use std::net::TcpStream;
#[cfg(unix)]
use std::os::unix::net::UnixStream;
use crate::{client::Position, config::DEFAULT_PORT};
use super::{FrontendEvent, FrontendNotify};
pub fn run() -> Result<()> {
#[cfg(unix)]
let socket_path = super::FrontendListener::socket_path()?;
#[cfg(unix)]
let Ok(mut tx) = UnixStream::connect(&socket_path) else {
return Err(anyhow!("Could not connect to lan-mouse-socket"));
};
#[cfg(windows)]
let Ok(mut tx) = TcpStream::connect("127.0.0.1:5252".parse::<SocketAddrV4>().unwrap()) else {
let Ok(mut tx) = super::wait_for_service() else {
return Err(anyhow!("Could not connect to lan-mouse-socket"));
};
@@ -40,7 +25,7 @@ pub fn run() -> Result<()> {
loop {
let mut buf = String::new();
match std::io::stdin().read_line(&mut buf) {
Ok(0) => break,
Ok(0) => return,
Ok(len) => {
if let Some(events) = parse_cmd(buf, len) {
for event in events.iter() {
@@ -63,14 +48,16 @@ pub fn run() -> Result<()> {
}
}
Err(e) => {
log::error!("error reading from stdin: {e}");
break;
if e.kind() != ErrorKind::UnexpectedEof {
log::error!("error reading from stdin: {e}");
}
return;
}
}
}
})?;
let writer = thread::Builder::new()
let _ = thread::Builder::new()
.name("cli-frontend-notify".to_string())
.spawn(move || {
loop {
@@ -139,7 +126,7 @@ pub fn run() -> Result<()> {
}
})?;
match reader.join() {
Ok(_) => (),
Ok(_) => {}
Err(e) => {
let msg = match (e.downcast_ref::<&str>(), e.downcast_ref::<String>()) {
(Some(&s), _) => s,
@@ -149,17 +136,6 @@ pub fn run() -> Result<()> {
log::error!("reader thread paniced: {msg}");
}
}
match writer.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!("writer thread paniced: {msg}");
}
}
Ok(())
}

View File

@@ -14,7 +14,7 @@ use adw::Application;
use gtk::{
gdk::Display,
gio::{SimpleAction, SimpleActionGroup},
glib::{clone, MainContext, Priority},
glib::clone,
prelude::*,
subclass::prelude::ObjectSubclassIsExt,
CssProvider, IconTheme,
@@ -27,7 +27,17 @@ use super::FrontendNotify;
pub fn run() -> glib::ExitCode {
log::debug!("running gtk frontend");
#[cfg(windows)]
let ret = std::thread::Builder::new()
.stack_size(8 * 1024 * 1024) // https://gitlab.gnome.org/GNOME/gtk/-/commit/52dbb3f372b2c3ea339e879689c1de535ba2c2c3 -> caused crash on windows
.name("gtk".into())
.spawn(gtk_main)
.unwrap()
.join()
.unwrap();
#[cfg(not(windows))]
let ret = gtk_main();
log::debug!("frontend exited");
ret
}
@@ -81,7 +91,7 @@ fn build_ui(app: &Application) {
};
log::debug!("connected to lan-mouse-socket");
let (sender, receiver) = MainContext::channel::<FrontendNotify>(Priority::default());
let (sender, receiver) = async_channel::bounded(10);
gio::spawn_blocking(move || {
match loop {
@@ -105,7 +115,7 @@ fn build_ui(app: &Application) {
// parse json
let json = str::from_utf8(&buf).unwrap();
match serde_json::from_str(json) {
Ok(notify) => sender.send(notify).unwrap(),
Ok(notify) => sender.send_blocking(notify).unwrap(),
Err(e) => log::error!("{e}"),
}
} {
@@ -116,8 +126,9 @@ fn build_ui(app: &Application) {
let window = Window::new(app);
window.imp().stream.borrow_mut().replace(tx);
receiver.attach(None, clone!(@weak window => @default-return glib::ControlFlow::Break,
move |notify| {
glib::spawn_future_local(clone!(@weak window => async move {
loop {
let notify = receiver.recv().await.unwrap();
match notify {
FrontendNotify::NotifyClientCreate(client, hostname, port, position) => {
window.new_client(client, hostname, port, position, false);
@@ -158,9 +169,8 @@ fn build_ui(app: &Application) {
window.imp().set_port(port);
}
}
glib::ControlFlow::Continue
}
));
}));
let action_request_client_update =
SimpleAction::new("request-client-update", Some(&u32::static_variant_type()));
@@ -175,7 +185,7 @@ fn build_ui(app: &Application) {
let index = param.unwrap()
.get::<u32>()
.unwrap();
let Some(client) = window.clients().item(index as u32) else {
let Some(client) = window.clients().item(index) else {
return;
};
let client = client.downcast_ref::<ClientObject>().unwrap();

View File

@@ -63,7 +63,7 @@ impl ClientRow {
let port_binding = client_object
.bind_property("port", &self.imp().port.get(), "text")
.transform_from(|_, v: String| {
if v == "" {
if v.is_empty() {
Some(DEFAULT_PORT as u32)
} else {
Some(v.parse::<u16>().unwrap_or(DEFAULT_PORT) as u32)

View File

@@ -85,16 +85,13 @@ impl Window {
}
pub fn client_idx(&self, handle: ClientHandle) -> Option<usize> {
self.clients()
.iter::<ClientObject>()
.position(|c| {
if let Ok(c) = c {
c.handle() == handle
} else {
false
}
})
.map(|p| p as usize)
self.clients().iter::<ClientObject>().position(|c| {
if let Ok(c) = c {
c.handle() == handle
} else {
false
}
})
}
pub fn delete_client(&self, handle: ClientHandle) {
@@ -117,7 +114,7 @@ impl Window {
pub fn request_port_change(&self) {
let port = self.imp().port_entry.get().text().to_string();
if let Ok(port) = u16::from_str_radix(port.as_str(), 10) {
if let Ok(port) = port.as_str().parse::<u16>() {
self.request(FrontendEvent::ChangePort(port));
} else {
self.request(FrontendEvent::ChangePort(DEFAULT_PORT));

View File

@@ -1,7 +1,9 @@
use std::{
cell::{Cell, RefCell},
os::unix::net::UnixStream,
};
use std::cell::{Cell, RefCell};
#[cfg(windows)]
use std::net::TcpStream;
#[cfg(unix)]
use std::os::unix::net::UnixStream;
use adw::subclass::prelude::*;
use adw::{
@@ -29,7 +31,10 @@ pub struct Window {
#[template_child]
pub toast_overlay: TemplateChild<ToastOverlay>,
pub clients: RefCell<Option<gio::ListStore>>,
#[cfg(unix)]
pub stream: RefCell<Option<UnixStream>>,
#[cfg(windows)]
pub stream: RefCell<Option<TcpStream>>,
pub port: Cell<u16>,
}

View File

@@ -1,48 +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

@@ -9,4 +9,4 @@ pub mod producer;
pub mod backend;
pub mod frontend;
pub mod ioutils;
pub mod scancode;

View File

@@ -2,15 +2,9 @@ use anyhow::Result;
use std::process::{self, Child, Command};
use env_logger::Env;
use lan_mouse::{
config::Config,
consumer,
frontend::{self, FrontendListener},
producer,
server::Server,
};
use lan_mouse::{config::Config, frontend, server::Server};
use tokio::{join, task::LocalSet};
use tokio::task::LocalSet;
pub fn main() {
// init logging
@@ -42,8 +36,19 @@ pub fn run() -> Result<()> {
} else {
// otherwise start the service as a child process and
// run a frontend
start_service()?;
let mut service = start_service()?;
frontend::run_frontend(&config)?;
log::info!("terminating service");
#[cfg(unix)]
{
// on unix we give the service a chance to terminate gracefully
let pid = service.id() as libc::pid_t;
unsafe {
libc::kill(pid, libc::SIGINT);
}
service.wait()?;
}
service.kill()?;
}
anyhow::Ok(())
@@ -58,27 +63,12 @@ fn run_service(config: &Config) -> Result<()> {
// run async event loop
runtime.block_on(LocalSet::new().run_until(async {
// create frontend communication adapter
let frontend_adapter = match FrontendListener::new().await {
Some(Err(e)) => return Err(e),
Some(Ok(f)) => f,
None => {
// none means some other instance is already running
log::info!("service already running, exiting");
return anyhow::Ok(());
}
};
// create event producer and consumer
let (producer, consumer) = join!(producer::create(), consumer::create(),);
let (producer, consumer) = (producer?, consumer?);
// create server
let mut event_server = Server::new(config, frontend_adapter, consumer, producer).await?;
// run main loop
log::info!("Press Ctrl+Alt+Shift+Super to release the mouse");
// run event loop
event_server.run().await?;
let server = Server::new(config);
server.run().await?;
log::debug!("service exiting");
anyhow::Ok(())
}))?;

View File

@@ -1,4 +1,3 @@
use anyhow::Result;
use std::io;
use futures_core::Stream;
@@ -9,83 +8,54 @@ use crate::{
event::Event,
};
#[cfg(all(unix, not(target_os = "macos")))]
use std::env;
#[cfg(all(unix, not(target_os = "macos")))]
enum Backend {
LayerShell,
Libei,
X11,
}
pub async fn create() -> Result<Box<dyn EventProducer>> {
pub async fn create() -> Box<dyn EventProducer> {
#[cfg(target_os = "macos")]
return Ok(Box::new(producer::macos::MacOSProducer::new()));
match producer::macos::MacOSProducer::new() {
Ok(p) => return Box::new(p),
Err(e) => log::info!("macos event producer not available: {e}"),
}
#[cfg(windows)]
return Ok(Box::new(producer::windows::WindowsProducer::new()));
#[cfg(all(unix, not(target_os = "macos")))]
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");
match env::var("XDG_CURRENT_DESKTOP") {
Ok(desktop) => match desktop.as_str() {
"GNOME" => {
log::info!("XDG_CURRENT_DESKTOP = GNOME -> using libei backend");
Backend::Libei
}
d => {
log::info!("XDG_CURRENT_DESKTOP = {d} -> using layer_shell backend");
Backend::LayerShell
}
},
Err(_) => {
log::warn!("XDG_CURRENT_DESKTOP not set! Assuming layer_shell support -> using layer_shell backend");
Backend::LayerShell
}
}
}
_ => panic!("unknown XDG_SESSION_TYPE"),
},
Err(_) => {
panic!("could not detect session type: XDG_SESSION_TYPE environment variable not set!")
}
};
#[cfg(all(unix, not(target_os = "macos")))]
match backend {
Backend::X11 => {
#[cfg(not(feature = "x11"))]
panic!("feature x11 not enabled");
#[cfg(feature = "x11")]
Ok(Box::new(producer::x11::X11Producer::new()))
}
Backend::LayerShell => {
#[cfg(not(feature = "wayland"))]
panic!("feature wayland not enabled");
#[cfg(feature = "wayland")]
Ok(Box::new(producer::wayland::WaylandEventProducer::new()?))
}
Backend::Libei => {
#[cfg(not(feature = "libei"))]
panic!("feature libei not enabled");
#[cfg(feature = "libei")]
Ok(Box::new(producer::libei::LibeiProducer::new()?))
}
match producer::windows::WindowsProducer::new() {
Ok(p) => return Box::new(p),
Err(e) => log::info!("windows event producer not available: {e}"),
}
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
match producer::libei::LibeiProducer::new() {
Ok(p) => {
log::info!("using libei event producer");
return Box::new(p);
}
Err(e) => log::info!("libei event producer not available: {e}"),
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
match producer::wayland::WaylandEventProducer::new() {
Ok(p) => {
log::info!("using layer-shell event producer");
return Box::new(p);
}
Err(e) => log::info!("layer_shell event producer not available: {e}"),
}
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
match producer::x11::X11Producer::new() {
Ok(p) => {
log::info!("using x11 event producer");
return Box::new(p);
}
Err(e) => log::info!("x11 event producer not available: {e}"),
}
log::error!("falling back to dummy event producer");
Box::new(producer::dummy::DummyProducer::new())
}
pub trait EventProducer: Stream<Item = io::Result<(ClientHandle, Event)>> + Unpin {
/// notify event producer of configuration changes
fn notify(&mut self, event: ClientEvent);
fn notify(&mut self, event: ClientEvent) -> io::Result<()>;
/// release mouse
fn release(&mut self);
fn release(&mut self) -> io::Result<()>;
}

698
src/scancode.rs Normal file
View File

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

File diff suppressed because it is too large Load Diff