Compare commits

...

46 Commits

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

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

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

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

There is one case that requires a Leave event:

Consider a Sending client A and receiving Client B.

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

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

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

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

* remove Path (not required anymore)

* remove "Test" from name

* add German name

* Update description to match GitHub description

---------

Co-authored-by: Ferdinand Schober <ferdinandschober20@gmail.com>
2023-12-10 23:15:53 +01:00
Ferdinand Schober
b472b56b10 fix a compiler warning 2023-12-10 14:26:05 +01:00
Ferdinand Schober
18edb0dbad update screenshot light / dark 2023-12-10 13:17:32 +01:00
42 changed files with 2690 additions and 1363 deletions

View File

@@ -30,14 +30,52 @@ jobs:
windows-release-build: windows-release-build:
runs-on: windows-latest runs-on: windows-latest
steps: 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 - uses: actions/checkout@v3
- name: Release Build - name: Release Build
run: cargo build --release 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 - name: Upload build artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: lan-mouse-windows name: lan-mouse-windows
path: target/release/lan-mouse.exe path: lan-mouse-windows.zip
macos-release-build: macos-release-build:
runs-on: macos-latest runs-on: macos-latest
@@ -46,12 +84,14 @@ jobs:
- name: install dependencies - name: install dependencies
run: brew install gtk4 libadwaita run: brew install gtk4 libadwaita
- name: Release Build - name: Release Build
run: cargo build --release run: |
cargo build --release
cp target/release/lan-mouse lan-mouse-macos-intel
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: lan-mouse-macos name: lan-mouse-macos
path: target/release/lan-mouse path: lan-mouse-macos-intel
pre-release: pre-release:
name: "Pre Release" name: "Pre Release"
@@ -69,5 +109,5 @@ jobs:
title: "Development Build" title: "Development Build"
files: | files: |
lan-mouse-linux/lan-mouse lan-mouse-linux/lan-mouse
lan-mouse-windows/lan-mouse.exe lan-mouse-macos/lan-mouse-macos-intel
lan-mouse-macos/lan-mouse lan-mouse-windows/lan-mouse-windows.zip

View File

@@ -25,6 +25,10 @@ jobs:
run: cargo build --verbose run: cargo build --verbose
- name: Run tests - name: Run tests
run: cargo test --verbose 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 - name: Upload build artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
@@ -37,15 +41,55 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - 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 - name: Build
run: cargo build --verbose run: cargo build --verbose
- name: Run tests - name: Run tests
run: cargo test --verbose 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 - name: Upload build artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: lan-mouse-windows name: lan-mouse-windows
path: target/debug/lan-mouse.exe path: |
target/debug/lan-mouse.exe
target/debug/*.dll
build-macos: build-macos:
runs-on: macos-latest runs-on: macos-latest
@@ -57,6 +101,10 @@ jobs:
run: cargo build --verbose run: cargo build --verbose
- name: Run tests - name: Run tests
run: cargo test --verbose 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 - name: Upload build artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:

View File

@@ -26,14 +26,52 @@ jobs:
windows-release-build: windows-release-build:
runs-on: windows-latest runs-on: windows-latest
steps: 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 - uses: actions/checkout@v3
- name: Release Build - name: Release Build
run: cargo build --release 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 - name: Upload build artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: lan-mouse-windows name: lan-mouse-windows
path: target/release/lan-mouse.exe path: lan-mouse-windows.zip
macos-release-build: macos-release-build:
runs-on: macos-latest runs-on: macos-latest
@@ -42,12 +80,14 @@ jobs:
- name: install dependencies - name: install dependencies
run: brew install gtk4 libadwaita run: brew install gtk4 libadwaita
- name: Release Build - name: Release Build
run: cargo build --release run: |
cargo build --release
cp target/release/lan-mouse lan-mouse-macos-intel
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: lan-mouse-macos name: lan-mouse-macos
path: target/release/lan-mouse path: lan-mouse-macos-intel
tagged-release: tagged-release:
name: "Tagged Release" name: "Tagged Release"
@@ -63,5 +103,5 @@ jobs:
prerelease: false prerelease: false
files: | files: |
lan-mouse-linux/lan-mouse lan-mouse-linux/lan-mouse
lan-mouse-windows/lan-mouse.exe lan-mouse-macos/lan-mouse-macos-intel
lan-mouse-macos/lan-mouse lan-mouse-windows/lan-mouse-windows.zip

2
.gitignore vendored
View File

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

711
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

125
README.md
View File

@@ -1,13 +1,17 @@
# Lan Mouse # Lan Mouse
- _Now with a gtk frontend_
Lan Mouse is a mouse and keyboard sharing software similar to universal-control on Apple devices. 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. 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) - _Now with a gtk frontend_
![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)
<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](https://symless.com/synergy), [Share Mouse](https://www.sharemouse.com/de/).
@@ -28,13 +32,79 @@ input capture (to send events *to* other clients) on different operating systems
| Wayland (wlroots) | :heavy_check_mark: | :heavy_check_mark: | | Wayland (wlroots) | :heavy_check_mark: | :heavy_check_mark: |
| Wayland (KDE) | :heavy_check_mark: | :heavy_check_mark: | | Wayland (KDE) | :heavy_check_mark: | :heavy_check_mark: |
| Wayland (Gnome) | :heavy_check_mark: | WIP | | Wayland (Gnome) | :heavy_check_mark: | WIP |
| X11 | (WIP) | WIP | | X11 | :heavy_check_mark: | WIP |
| Windows | ( :heavy_check_mark: ) | WIP | | Windows | :heavy_check_mark: | WIP |
| MacOS | ( :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. Keycode translation is not yet implemented so on MacOS only mouse emulation works as of right now.
## Build and Run ## 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: Build in release mode:
```sh ```sh
cargo build --release cargo build --release
@@ -47,22 +117,23 @@ cargo run --release
### Conditional Compilation ### 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 Depending on the toolchain used, support for other platforms is omitted
automatically (it does not make sense to build a Windows `.exe` with automatically (it does not make sense to build a Windows `.exe` with
support for x11 and wayland backends). 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. a Linux system.
This is possible through 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 E.g. if only wayland support is needed, the following command produces
an executable with just support for wayland: an executable with just support for wayland:
```sh ```sh
cargo build --no-default-features --features wayland cargo build --no-default-features --features wayland
``` ```
For a detailed list of available features, checkout the [Cargo.toml](./Cargo.toml)
## Usage ## Usage
### Gtk Frontend ### Gtk Frontend
@@ -135,22 +206,19 @@ port = 4242
Where `left` can be either `left`, `right`, `top` or `bottom`. Where `left` can be either `left`, `right`, `top` or `bottom`.
## Roadmap ## Roadmap
- [x] Capture the actual mouse events on the server side via a wayland client and send them to the client - [x] Graphical frontend (gtk + libadwaita)
- [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] respect xdg-config-home for config file location. - [x] respect xdg-config-home for config file location.
- [x] IP Address switching
- [x] Liveness tracking Automatically ungrab mouse when client unreachable
- [ ] Liveness tracking: Automatically release keys, when server offline
- [ ] X11 Input Capture
- [ ] Windows Input Capture
- [ ] MacOS Input Capture
- [ ] MaxOS KeyCode Translation
- [ ] Latency measurement and visualization
- [ ] Bandwidth usage measurement and visualization
- [ ] Clipboard support
- [ ] *Encryption*
## Protocol ## Protocol
Currently *all* mouse and keyboard events are sent via **UDP** for performance reasons. Currently *all* mouse and keyboard events are sent via **UDP** for performance reasons.
@@ -222,10 +290,9 @@ it is however not exposed to third party applications.
The recommended way to emulate input on KDE is the 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). [freedesktop remote-desktop-portal](https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.RemoteDesktop).
#### Gnome (TODO) #### Gnome
Gnome uses [libei](https://gitlab.freedesktop.org/libinput/libei) for input emulation, 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 Input on wayland. which has the goal to become the general approach for emulating and capturing Input on Wayland.
### Input capture ### Input capture

View File

@@ -1,6 +1,5 @@
fn main() { fn main() {
// composite_templates // composite_templates
#[cfg(unix)]
glib_build_tools::compile_resources( glib_build_tools::compile_resources(
&["resources"], &["resources"],
"resources/resources.gresource.xml", "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.0

View File

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

View File

@@ -15,3 +15,6 @@ pub mod libei;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub mod 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)?; let context = ei::Context::new(stream)?;
context.flush()?; context.flush()?;
let events = EiEventStream::new(context.clone())?; let events = EiEventStream::new(context.clone())?;
return Ok(Self { Ok(Self {
handshake: false, handshake: false,
context, context,
events, events,
@@ -96,7 +96,7 @@ impl LibeiConsumer {
capability_mask: 0, capability_mask: 0,
sequence: 0, sequence: 0,
serial: 0, serial: 0,
}); })
} }
} }
@@ -193,7 +193,7 @@ impl EventConsumer for LibeiConsumer {
}; };
let event = match event { let event = match event {
PendingRequestResult::Request(result) => result, PendingRequestResult::Request(result) => result,
PendingRequestResult::ProtocolError(e) => { PendingRequestResult::ParseError(e) => {
return Err(anyhow!("libei protocol violation: {e}")) return Err(anyhow!("libei protocol violation: {e}"))
} }
PendingRequestResult::InvalidObject(e) => return Err(anyhow!("invalid object {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 crate::event::{Event, KeyboardEvent, PointerEvent};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use async_trait::async_trait; use async_trait::async_trait;
use core_graphics::display::CGPoint; use core_graphics::display::{CGDisplayBounds, CGMainDisplayID, CGPoint};
use core_graphics::event::{ use core_graphics::event::{
CGEvent, CGEventTapLocation, CGEventType, CGMouseButton, ScrollEventUnit, CGEvent, CGEventTapLocation, CGEventType, CGMouseButton, EventField, ScrollEventUnit,
}; };
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID}; use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
use std::ops::{Index, IndexMut}; use std::ops::{Index, IndexMut};
@@ -78,6 +78,16 @@ impl EventConsumer for MacOSConsumer {
relative_x, relative_x,
relative_y, 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() { let mut mouse_location = match self.get_mouse_location() {
Some(l) => l, Some(l) => l,
None => { None => {
@@ -85,8 +95,9 @@ impl EventConsumer for MacOSConsumer {
return; 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; let mut event_type = CGEventType::MouseMoved;
if self.button_state.left { if self.button_state.left {
@@ -108,6 +119,14 @@ impl EventConsumer for MacOSConsumer {
return; 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); event.post(CGEventTapLocation::HID);
} }
PointerEvent::Button { PointerEvent::Button {
@@ -140,7 +159,7 @@ impl EventConsumer for MacOSConsumer {
} }
}; };
// store button state // 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 location = self.get_mouse_location().unwrap();
let event = match CGEvent::new_mouse_event( let event = match CGEvent::new_mouse_event(
@@ -209,9 +228,7 @@ impl EventConsumer for MacOSConsumer {
} }
KeyboardEvent::Modifiers { .. } => {} KeyboardEvent::Modifiers { .. } => {}
}, },
Event::Release() => {} _ => (),
Event::Ping() => {}
Event::Pong() => {}
} }
} }

View File

@@ -1,8 +1,11 @@
use crate::{ use crate::{
consumer::EventConsumer, consumer::EventConsumer,
event::{KeyboardEvent, PointerEvent}, event::{KeyboardEvent, PointerEvent},
scancode,
}; };
use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use winapi::um::winuser::{SendInput, KEYEVENTF_EXTENDEDKEY};
use winapi::{ use winapi::{
self, self,
um::winuser::{ um::winuser::{
@@ -21,8 +24,8 @@ use crate::{
pub struct WindowsConsumer {} pub struct WindowsConsumer {}
impl WindowsConsumer { impl WindowsConsumer {
pub fn new() -> Self { pub fn new() -> Result<Self> {
Self {} Ok(Self {})
} }
} }
@@ -76,8 +79,8 @@ fn send_mouse_input(mi: MOUSEINPUT) {
u: std::mem::transmute(mi), u: std::mem::transmute(mi),
}; };
winapi::um::winuser::SendInput( SendInput(
1 as u32, 1_u32,
&mut input as LPINPUT, &mut input as LPINPUT,
std::mem::size_of::<INPUT>() as i32, std::mem::size_of::<INPUT>() as i32,
); );
@@ -141,10 +144,17 @@ fn scroll(axis: u8, value: f64) {
} }
fn key_event(key: u32, state: u8) { 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 { let ki = KEYBDINPUT {
wVk: 0, wVk: 0,
wScan: key as u16, wScan: scancode,
dwFlags: KEYEVENTF_SCANCODE dwFlags: KEYEVENTF_SCANCODE
| if extended { KEYEVENTF_EXTENDEDKEY } else { 0 }
| match state { | match state {
0 => KEYEVENTF_KEYUP, 0 => KEYEVENTF_KEYUP,
1 => 0u32, 1 => 0u32,
@@ -163,6 +173,26 @@ fn send_keyboard_input(ki: KEYBDINPUT) {
u: std::mem::zeroed(), u: std::mem::zeroed(),
}; };
*input.u.ki_mut() = ki; *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 async_trait::async_trait;
use std::collections::HashMap; use std::collections::HashMap;
use std::io; use std::io;
use std::os::fd::OwnedFd; use std::os::fd::{AsFd, OwnedFd};
use std::os::unix::prelude::AsRawFd;
use wayland_client::backend::WaylandError; use wayland_client::backend::WaylandError;
use wayland_client::WEnum; use wayland_client::WEnum;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use wayland_client::globals::BindError;
use wayland_client::protocol::wl_keyboard::{self, WlKeyboard}; use wayland_client::protocol::wl_keyboard::{self, WlKeyboard};
use wayland_client::protocol::wl_pointer::{Axis, ButtonState}; use wayland_client::protocol::wl_pointer::{Axis, ButtonState};
use wayland_client::protocol::wl_seat::WlSeat; 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, zwp_virtual_keyboard_v1::ZwpVirtualKeyboardV1 as Vk,
}; };
use wayland_protocols_plasma::fake_input::client::org_kde_kwin_fake_input::OrgKdeKwinFakeInput;
use wayland_client::{ use wayland_client::{
delegate_noop, delegate_noop,
globals::{registry_queue_init, GlobalListContents}, globals::{registry_queue_init, GlobalListContents},
@@ -34,17 +30,13 @@ use wayland_client::{
use crate::event::{Event, KeyboardEvent, PointerEvent}; use crate::event::{Event, KeyboardEvent, PointerEvent};
enum VirtualInputManager {
Wlroots { vpm: VpManager, vkm: VkManager },
Kde { fake_input: OrgKdeKwinFakeInput },
}
struct State { struct State {
keymap: Option<(u32, OwnedFd, u32)>, keymap: Option<(u32, OwnedFd, u32)>,
input_for_client: HashMap<ClientHandle, VirtualInput>, input_for_client: HashMap<ClientHandle, VirtualInput>,
seat: wl_seat::WlSeat, seat: wl_seat::WlSeat,
virtual_input_manager: VirtualInputManager,
qh: QueueHandle<Self>, qh: QueueHandle<Self>,
vpm: VpManager,
vkm: VkManager,
} }
// App State, implements Dispatch event handlers // App State, implements Dispatch event handlers
@@ -56,8 +48,8 @@ pub(crate) struct WlrootsConsumer {
impl WlrootsConsumer { impl WlrootsConsumer {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
let conn = Connection::connect_to_env().unwrap(); let conn = Connection::connect_to_env()?;
let (globals, queue) = registry_queue_init::<State>(&conn).unwrap(); let (globals, queue) = registry_queue_init::<State>(&conn)?;
let qh = queue.handle(); let qh = queue.handle();
let seat: wl_seat::WlSeat = match globals.bind(&qh, 7..=8, ()) { 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")), Err(_) => return Err(anyhow!("wl_seat >= v7 not supported")),
}; };
let vpm: Result<VpManager, BindError> = globals.bind(&qh, 1..=1, ()); let vpm: VpManager = globals.bind(&qh, 1..=1, ())?;
let vkm: Result<VkManager, BindError> = globals.bind(&qh, 1..=1, ()); let vkm: VkManager = globals.bind(&qh, 1..=1, ())?;
let fake_input: Result<OrgKdeKwinFakeInput, BindError> = globals.bind(&qh, 4..=4, ());
let virtual_input_manager = match (vpm, vkm, fake_input) {
(Ok(vpm), Ok(vkm), _) => VirtualInputManager::Wlroots { vpm, vkm },
(_, _, Ok(fake_input)) => {
fake_input.authenticate(
"lan-mouse".into(),
"Allow remote clients to control this device".into(),
);
VirtualInputManager::Kde { fake_input }
}
(Err(e1), Err(e2), Err(e3)) => {
log::warn!("zwlr_virtual_pointer_v1: {e1}");
log::warn!("zwp_virtual_keyboard_v1: {e2}");
log::warn!("org_kde_kwin_fake_input: {e3}");
log::error!("neither wlroots nor kde input emulation protocol supported!");
return Err(anyhow!("could not create event consumer"));
}
_ => {
panic!()
}
};
let input_for_client: HashMap<ClientHandle, VirtualInput> = HashMap::new(); let input_for_client: HashMap<ClientHandle, VirtualInput> = HashMap::new();
@@ -98,7 +68,8 @@ impl WlrootsConsumer {
keymap: None, keymap: None,
input_for_client, input_for_client,
seat, seat,
virtual_input_manager, vpm,
vkm,
qh, qh,
}, },
queue, queue,
@@ -118,29 +89,19 @@ impl WlrootsConsumer {
impl State { impl State {
fn add_client(&mut self, client: ClientHandle) { fn add_client(&mut self, client: ClientHandle) {
// create virtual input devices let pointer: Vp = self.vpm.create_virtual_pointer(None, &self.qh, ());
match &self.virtual_input_manager { let keyboard: Vk = self.vkm.create_virtual_keyboard(&self.seat, &self.qh, ());
VirtualInputManager::Wlroots { vpm, vkm } => {
let pointer: Vp = vpm.create_virtual_pointer(None, &self.qh, ());
let keyboard: Vk = vkm.create_virtual_keyboard(&self.seat, &self.qh, ());
// TODO: use server side keymap // TODO: use server side keymap
if let Some((format, fd, size)) = self.keymap.as_ref() { if let Some((format, fd, size)) = self.keymap.as_ref() {
keyboard.keymap(*format, fd.as_raw_fd(), *size); keyboard.keymap(*format, fd.as_fd(), *size);
} else { } else {
panic!("no keymap"); 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);
}
} }
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) {} async fn destroy(&mut self) {}
} }
enum VirtualInput { struct VirtualInput {
Wlroots { pointer: Vp, keyboard: Vk }, pointer: Vp,
Kde { fake_input: OrgKdeKwinFakeInput }, keyboard: Vk,
} }
impl VirtualInput { impl VirtualInput {
@@ -208,94 +169,37 @@ impl VirtualInput {
time, time,
relative_x, relative_x,
relative_y, relative_y,
} => match self { } => self.pointer.motion(time, relative_x, relative_y),
VirtualInput::Wlroots {
pointer,
keyboard: _,
} => {
pointer.motion(time, relative_x, relative_y);
}
VirtualInput::Kde { fake_input } => {
fake_input.pointer_motion(relative_y, relative_y);
}
},
PointerEvent::Button { PointerEvent::Button {
time, time,
button, button,
state, state,
} => { } => {
let state: ButtonState = state.try_into()?; let state: ButtonState = state.try_into()?;
match self { self.pointer.button(time, button, state);
VirtualInput::Wlroots {
pointer,
keyboard: _,
} => {
pointer.button(time, button, state);
}
VirtualInput::Kde { fake_input } => {
fake_input.button(button, state as u32);
}
}
} }
PointerEvent::Axis { time, axis, value } => { PointerEvent::Axis { time, axis, value } => {
let axis: Axis = (axis as u32).try_into()?; let axis: Axis = (axis as u32).try_into()?;
match self { self.pointer.axis(time, axis, value);
VirtualInput::Wlroots { self.pointer.frame();
pointer,
keyboard: _,
} => {
pointer.axis(time, axis, value);
pointer.frame();
}
VirtualInput::Kde { fake_input } => {
fake_input.axis(axis as u32, value);
}
}
} }
PointerEvent::Frame {} => match self { PointerEvent::Frame {} => self.pointer.frame(),
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();
}
_ => {}
} }
self.pointer.frame();
} }
Event::Keyboard(e) => match e { Event::Keyboard(e) => match e {
KeyboardEvent::Key { time, key, state } => match self { KeyboardEvent::Key { time, key, state } => {
VirtualInput::Wlroots { self.keyboard.key(time, key, state as u32);
pointer: _, }
keyboard,
} => {
keyboard.key(time, key, state as u32);
}
VirtualInput::Kde { fake_input } => {
fake_input.keyboard_key(key, state as u32);
}
},
KeyboardEvent::Modifiers { KeyboardEvent::Modifiers {
mods_depressed, mods_depressed,
mods_latched, mods_latched,
mods_locked, mods_locked,
group, group,
} => match self { } => {
VirtualInput::Wlroots { self.keyboard
pointer: _, .modifiers(mods_depressed, mods_latched, mods_locked, group);
keyboard, }
} => {
keyboard.modifiers(mods_depressed, mods_latched, mods_locked, group);
}
VirtualInput::Kde { fake_input: _ } => {}
},
}, },
_ => {} _ => {}
} }
@@ -307,7 +211,6 @@ delegate_noop!(State: Vp);
delegate_noop!(State: Vk); delegate_noop!(State: Vk);
delegate_noop!(State: VpManager); delegate_noop!(State: VpManager);
delegate_noop!(State: VkManager); delegate_noop!(State: VkManager);
delegate_noop!(State: OrgKdeKwinFakeInput);
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for State { impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for State {
fn event( fn event(
@@ -330,11 +233,8 @@ impl Dispatch<WlKeyboard, ()> for State {
_: &Connection, _: &Connection,
_: &QueueHandle<Self>, _: &QueueHandle<Self>,
) { ) {
match event { if let wl_keyboard::Event::Keymap { format, fd, size } = event {
wl_keyboard::Event::Keymap { format, fd, size } => { state.keymap = Some((u32::from(format), fd, size));
state.keymap = Some((u32::from(format), fd, size));
}
_ => {}
} }
} }
} }

View File

@@ -1,8 +1,18 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait; use async_trait::async_trait;
use std::ptr; 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 { pub struct X11Consumer {
display: *mut xlib::Display, display: *mut xlib::Display,
@@ -11,21 +21,80 @@ pub struct X11Consumer {
unsafe impl Send for X11Consumer {} unsafe impl Send for X11Consumer {}
impl X11Consumer { impl X11Consumer {
pub fn new() -> Self { pub fn new() -> Result<Self> {
let display = unsafe { let display = unsafe {
match xlib::XOpenDisplay(ptr::null()) { match xlib::XOpenDisplay(ptr::null()) {
d if d == ptr::null::<xlib::Display>() as *mut xlib::Display => None, d if d == ptr::null::<xlib::Display>() as *mut xlib::Display => {
display => Some(display), Err(anyhow!("could not open display"))
}
display => Ok(display),
} }
}; }?;
let display = display.expect("could not open display"); Ok(Self { display })
Self { display }
} }
fn relative_motion(&self, dx: i32, dy: i32) { fn relative_motion(&self, dx: i32, dy: i32) {
unsafe { unsafe {
xtest::XTestFakeRelativeMotionEvent(self.display, dx, dy, 0, 0); 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) { async fn consume(&mut self, event: Event, _: ClientHandle) {
match event { match event {
Event::Pointer(pointer_event) => match pointer_event { Event::Pointer(pointer_event) => match pointer_event {
crate::event::PointerEvent::Motion { PointerEvent::Motion {
time: _, time: _,
relative_x, relative_x,
relative_y, relative_y,
} => { } => {
self.relative_motion(relative_x as i32, relative_y as i32); self.relative_motion(relative_x as i32, relative_y as i32);
} }
crate::event::PointerEvent::Button { .. } => {} PointerEvent::Button {
crate::event::PointerEvent::Axis { .. } => {} time: _,
crate::event::PointerEvent::Frame {} => {} 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) { async fn notify(&mut self, _: crate::client::ClientEvent) {

View File

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

View File

@@ -1,10 +1,17 @@
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
pub mod libei; pub mod libei;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub mod macos; pub mod macos;
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))] #[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
pub mod wayland; pub mod wayland;
#[cfg(windows)] #[cfg(windows)]
pub mod windows; pub mod windows;
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
pub mod x11; pub mod x11;
/// fallback event producer
pub mod dummy;

View File

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

View File

@@ -1,4 +1,4 @@
use anyhow::Result; use anyhow::{anyhow, Result};
use std::{io, task::Poll}; use std::{io, task::Poll};
use futures_core::Stream; use futures_core::Stream;
@@ -9,7 +9,7 @@ pub struct LibeiProducer {}
impl LibeiProducer { impl LibeiProducer {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
Ok(Self {}) Err(anyhow!("not implemented"))
} }
} }

View File

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

View File

@@ -10,7 +10,7 @@ use std::{
collections::VecDeque, collections::VecDeque,
env, env,
io::{self, ErrorKind}, io::{self, ErrorKind},
os::fd::{OwnedFd, RawFd}, os::fd::{AsFd, OwnedFd, RawFd},
pin::Pin, pin::Pin,
task::{ready, Context, Poll}, task::{ready, Context, Poll},
}; };
@@ -145,7 +145,7 @@ impl Window {
draw(&mut file, (width, height)); draw(&mut file, (width, height));
let pool = g let pool = g
.shm .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( let buffer = pool.create_buffer(
0, 0,
width as i32, width as i32,
@@ -159,7 +159,7 @@ impl Window {
let layer_surface = g.layer_shell.get_layer_surface( let layer_surface = g.layer_shell.get_layer_surface(
&surface, &surface,
Some(&output), Some(output),
Layer::Overlay, Layer::Overlay,
"LAN Mouse Sharing".into(), "LAN Mouse Sharing".into(),
qh, qh,
@@ -221,13 +221,7 @@ fn get_output_configuration(state: &State, pos: Position) -> Vec<(WlOutput, Outp
// as an opposite edge of a different output // as an opposite edge of a different output
let outputs: Vec<WlOutput> = edges let outputs: Vec<WlOutput> = edges
.iter() .iter()
.filter(|(_, edge)| { .filter(|(_, edge)| !opposite_edges.iter().map(|(_, e)| *e).any(|e| &e == edge))
opposite_edges
.iter()
.map(|(_, e)| *e)
.find(|e| e == edge)
.is_none()
})
.map(|(o, _)| o.clone()) .map(|(o, _)| o.clone())
.collect(); .collect();
state state
@@ -244,7 +238,7 @@ fn draw(f: &mut File, (width, height): (u32, u32)) {
for _ in 0..width { for _ in 0..width {
if env::var("LM_DEBUG_LAYER_SHELL").ok().is_some() { if env::var("LM_DEBUG_LAYER_SHELL").ok().is_some() {
// AARRGGBB // AARRGGBB
buf.write_all(&0xFF11d116u32.to_ne_bytes()).unwrap(); buf.write_all(&0xff11d116u32.to_ne_bytes()).unwrap();
} else { } else {
// AARRGGBB // AARRGGBB
buf.write_all(&0x00000000u32.to_ne_bytes()).unwrap(); buf.write_all(&0x00000000u32.to_ne_bytes()).unwrap();
@@ -330,7 +324,7 @@ impl WaylandEventProducer {
queue.flush()?; queue.flush()?;
// prepare reading wayland events // 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(); let wayland_fd = read_guard.connection_fd().try_clone_to_owned().unwrap();
std::mem::drop(read_guard); std::mem::drop(read_guard);
@@ -372,7 +366,15 @@ impl WaylandEventProducer {
log::debug!("{:#?}", i.1); 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); state.read_guard = Some(read_guard);
let inner = AsyncFd::new(Inner { queue, state })?; let inner = AsyncFd::new(Inner { queue, state })?;
@@ -468,7 +470,7 @@ impl State {
let outputs = get_output_configuration(self, pos); let outputs = get_output_configuration(self, pos);
outputs.iter().for_each(|(o, i)| { 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); let window = Rc::new(window);
self.client_for_window.push((window, client)); self.client_for_window.push((window, client));
}); });
@@ -490,16 +492,20 @@ impl Inner {
} }
} }
fn prepare_read(&mut self) { fn prepare_read(&mut self) -> io::Result<()> {
match self.queue.prepare_read() { loop {
Ok(r) => self.state.read_guard = Some(r), match self.queue.prepare_read() {
Err(WaylandError::Io(e)) => { None => match self.queue.dispatch_pending(&mut self.state) {
log::error!("error preparing read from wayland socket: {e}") 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) { fn dispatch_events(&mut self) {
@@ -545,19 +551,15 @@ impl EventProducer for WaylandEventProducer {
} }
ClientEvent::Destroy(handle) => { ClientEvent::Destroy(handle) => {
let inner = self.0.get_mut(); let inner = self.0.get_mut();
loop { // remove all windows corresponding to this client
// remove all windows corresponding to this client while let Some(i) = inner
if let Some(i) = inner .state
.state .client_for_window
.client_for_window .iter()
.iter() .position(|(_, c)| *c == handle)
.position(|(_, c)| *c == handle) {
{ inner.state.client_for_window.remove(i);
inner.state.client_for_window.remove(i); inner.state.focused = None;
inner.state.focused = None;
} else {
break;
}
} }
} }
} }
@@ -576,7 +578,6 @@ impl Stream for WaylandEventProducer {
type Item = io::Result<(ClientHandle, Event)>; type Item = io::Result<(ClientHandle, Event)>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { 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() { if let Some(event) = self.0.get_mut().state.pending_events.pop_front() {
return Poll::Ready(Some(Ok(event))); return Poll::Ready(Some(Ok(event)));
} }
@@ -590,7 +591,10 @@ impl Stream for WaylandEventProducer {
// read events // read events
while inner.read() { while inner.read() {
// prepare next read // prepare next read
inner.prepare_read(); match inner.prepare_read() {
Ok(_) => {}
Err(e) => return Poll::Ready(Some(Err(e))),
}
} }
// dispatch the events // dispatch the events
@@ -600,7 +604,10 @@ impl Stream for WaylandEventProducer {
inner.flush_events(); inner.flush_events();
// prepare for the next read // 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 // clear read readiness for tokio read guard
@@ -663,7 +670,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
.find(|(w, _c)| w.surface == surface) .find(|(w, _c)| w.surface == surface)
{ {
app.focused = Some((window.clone(), *client)); app.focused = Some((window.clone(), *client));
app.grab(&surface, pointer, serial.clone(), qh); app.grab(&surface, pointer, serial, qh);
} else { } else {
return; return;
} }
@@ -673,7 +680,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
.iter() .iter()
.find(|(w, _c)| w.surface == surface) .find(|(w, _c)| w.surface == surface)
.unwrap(); .unwrap();
app.pending_events.push_back((*client, Event::Release())); app.pending_events.push_back((*client, Event::Enter()));
} }
wl_pointer::Event::Leave { .. } => { wl_pointer::Event::Leave { .. } => {
app.ungrab(); app.ungrab();
@@ -764,10 +771,6 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for State {
}), }),
)); ));
} }
if mods_depressed == 77 {
// ctrl shift super alt
app.ungrab();
}
} }
wl_keyboard::Event::Keymap { wl_keyboard::Event::Keymap {
format: _, format: _,
@@ -834,7 +837,7 @@ impl Dispatch<ZwlrLayerSurfaceV1, ()> for State {
// client corresponding to the layer_surface // client corresponding to the layer_surface
let surface = &window.surface; let surface = &window.surface;
let buffer = &window.buffer; let buffer = &window.buffer;
surface.attach(Some(&buffer), 0, 0); surface.attach(Some(buffer), 0, 0);
layer_surface.ack_configure(serial); layer_surface.ack_configure(serial);
surface.commit(); surface.commit();
} }
@@ -869,16 +872,15 @@ impl Dispatch<wl_registry::WlRegistry, ()> for State {
name, name,
interface, interface,
version: _, version: _,
} => match interface.as_str() { } => {
"wl_output" => { if interface.as_str() == "wl_output" {
log::debug!("wl_output global"); log::debug!("wl_output global");
state state
.g .g
.outputs .outputs
.push(registry.bind::<wl_output::WlOutput, _, _>(name, 4, qh, ())) .push(registry.bind::<wl_output::WlOutput, _, _>(name, 4, qh, ()))
} }
_ => {} }
},
wl_registry::Event::GlobalRemove { .. } => {} wl_registry::Event::GlobalRemove { .. } => {}
_ => {} _ => {}
} }

View File

@@ -1,7 +1,7 @@
use anyhow::{anyhow, Result};
use core::task::{Context, Poll}; use core::task::{Context, Poll};
use futures::Stream; use futures::Stream;
use std::io::Result; use std::{io, pin::Pin};
use std::pin::Pin;
use crate::{ use crate::{
client::{ClientEvent, ClientHandle}, client::{ClientEvent, ClientHandle},
@@ -18,13 +18,13 @@ impl EventProducer for WindowsProducer {
} }
impl WindowsProducer { impl WindowsProducer {
pub(crate) fn new() -> Self { pub(crate) fn new() -> Result<Self> {
Self {} Err(anyhow!("not implemented"))
} }
} }
impl Stream for WindowsProducer { 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>> { fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Poll::Pending Poll::Pending
} }

View File

@@ -1,3 +1,4 @@
use anyhow::{anyhow, Result};
use std::io; use std::io;
use std::task::Poll; use std::task::Poll;
@@ -11,8 +12,8 @@ use crate::client::{ClientEvent, ClientHandle};
pub struct X11Producer {} pub struct X11Producer {}
impl X11Producer { impl X11Producer {
pub fn new() -> Self { pub fn new() -> Result<Self> {
Self {} Err(anyhow!("not implemented"))
} }
} }

View File

@@ -51,6 +51,8 @@ impl Display for Position {
pub struct Client { pub struct Client {
/// hostname of this client /// hostname of this client
pub hostname: Option<String>, pub hostname: Option<String>,
/// fix ips, determined by the user
pub fix_ips: Vec<IpAddr>,
/// unique handle to refer to the client. /// unique handle to refer to the client.
/// This way any event consumer / producer backend does not /// This way any event consumer / producer backend does not
/// need to know anything about a client other than its handle. /// need to know anything about a client other than its handle.
@@ -69,6 +71,7 @@ pub struct Client {
pub pos: Position, pub pos: Position,
} }
#[derive(Debug)]
pub enum ClientEvent { pub enum ClientEvent {
Create(ClientHandle, Position), Create(ClientHandle, Position),
Destroy(ClientHandle), Destroy(ClientHandle),
@@ -89,6 +92,12 @@ pub struct ClientManager {
clients: Vec<Option<ClientState>>, // HashMap likely not beneficial clients: Vec<Option<ClientState>>, // HashMap likely not beneficial
} }
impl Default for ClientManager {
fn default() -> Self {
Self::new()
}
}
impl ClientManager { impl ClientManager {
pub fn new() -> Self { pub fn new() -> Self {
Self { clients: vec![] } Self { clients: vec![] }
@@ -98,7 +107,7 @@ impl ClientManager {
pub fn add_client( pub fn add_client(
&mut self, &mut self,
hostname: Option<String>, hostname: Option<String>,
addrs: HashSet<IpAddr>, ips: HashSet<IpAddr>,
port: u16, port: u16,
pos: Position, pos: Position,
) -> ClientHandle { ) -> ClientHandle {
@@ -108,12 +117,16 @@ impl ClientManager {
// we dont know, which IP is initially active // we dont know, which IP is initially active
let active_addr = None; let active_addr = None;
// store fix ip addresses
let fix_ips = ips.iter().cloned().collect();
// map ip addresses to socket addresses // 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 // store the client
let client = Client { let client = Client {
hostname, hostname,
fix_ips,
handle, handle,
active_addr, active_addr,
addrs, addrs,
@@ -174,20 +187,20 @@ impl ClientManager {
} }
// returns an immutable reference to the client state corresponding to `client` // 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() self.clients.get(client as usize)?.as_ref()
} }
/// returns a mutable reference to the client state corresponding to `client` /// 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() self.clients.get_mut(client as usize)?.as_mut()
} }
pub fn enumerate(&self) -> Vec<(Client, bool)> { pub fn get_client_states(&self) -> impl Iterator<Item = &ClientState> {
self.clients self.clients.iter().filter_map(|x| x.as_ref())
.iter() }
.filter_map(|s| s.as_ref())
.map(|s| (s.client.clone(), s.active)) pub fn get_client_states_mut(&mut self) -> impl Iterator<Item = &mut ClientState> {
.collect() self.clients.iter_mut().filter_map(|x| x.as_mut())
} }
} }

View File

@@ -109,9 +109,9 @@ impl Config {
}; };
let frontend = match frontend { let frontend = match frontend {
#[cfg(all(unix, feature = "gtk"))] #[cfg(feature = "gtk")]
None => Frontend::Gtk, None => Frontend::Gtk,
#[cfg(any(not(feature = "gtk"), not(unix)))] #[cfg(not(feature = "gtk"))]
None => Frontend::Cli, None => Frontend::Cli,
Some(s) => match s.as_str() { Some(s) => match s.as_str() {
"cli" => Frontend::Cli, "cli" => Frontend::Cli,

View File

@@ -1,9 +1,6 @@
use async_trait::async_trait; use async_trait::async_trait;
use std::future; use std::future;
#[cfg(all(unix, not(target_os = "macos")))]
use std::env;
use crate::{ use crate::{
backend::consumer, backend::consumer,
client::{ClientEvent, ClientHandle}, client::{ClientEvent, ClientHandle},
@@ -11,15 +8,6 @@ use crate::{
}; };
use anyhow::Result; use anyhow::Result;
#[cfg(all(unix, not(target_os = "macos")))]
#[derive(Debug)]
enum Backend {
Wlroots,
X11,
RemoteDesktopPortal,
Libei,
}
#[async_trait] #[async_trait]
pub trait EventConsumer: Send { pub trait EventConsumer: Send {
async fn consume(&mut self, event: Event, client_handle: ClientHandle); async fn consume(&mut self, event: Event, client_handle: ClientHandle);
@@ -33,90 +21,58 @@ pub trait EventConsumer: Send {
async fn destroy(&mut self); async fn destroy(&mut self);
} }
pub async fn create() -> Result<Box<dyn EventConsumer>> { pub async fn create() -> Box<dyn EventConsumer> {
#[cfg(windows)] #[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")] #[cfg(target_os = "macos")]
return Ok(Box::new(consumer::macos::MacOSConsumer::new()?)); match consumer::macos::MacOSConsumer::new() {
Ok(c) => {
#[cfg(all(unix, not(target_os = "macos")))] log::info!("using macos event consumer");
let backend = match env::var("XDG_SESSION_TYPE") { return Box::new(c);
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()))
} }
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; use trust_dns_resolver::TokioAsyncResolver;
pub(crate) struct DnsResolver { pub struct DnsResolver {
resolver: TokioAsyncResolver, resolver: TokioAsyncResolver,
} }
impl DnsResolver { impl DnsResolver {

View File

@@ -4,9 +4,11 @@ use std::{
}; };
// FIXME // FIXME
pub(crate) const BTN_LEFT: u32 = 0x110; pub const BTN_LEFT: u32 = 0x110;
pub(crate) const BTN_RIGHT: u32 = 0x111; pub const BTN_RIGHT: u32 = 0x111;
pub(crate) const BTN_MIDDLE: u32 = 0x112; pub const BTN_MIDDLE: u32 = 0x112;
pub const BTN_BACK: u32 = 0x113;
pub const BTN_FORWARD: u32 = 0x114;
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum PointerEvent { pub enum PointerEvent {
@@ -45,10 +47,23 @@ pub enum KeyboardEvent {
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum Event { pub enum Event {
/// pointer event (motion / button / axis)
Pointer(PointerEvent), Pointer(PointerEvent),
/// keyboard events (key / modifiers)
Keyboard(KeyboardEvent), 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(), Ping(),
/// response to a ping event: this event signals that a client
/// is still alive but must otherwise be ignored
Pong(), Pong(),
} }
@@ -101,7 +116,8 @@ impl Display for Event {
match self { match self {
Event::Pointer(p) => write!(f, "{}", p), Event::Pointer(p) => write!(f, "{}", p),
Event::Keyboard(k) => write!(f, "{}", k), 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::Ping() => write!(f, "ping"),
Event::Pong() => write!(f, "pong"), Event::Pong() => write!(f, "pong"),
} }
@@ -111,11 +127,12 @@ impl Display for Event {
impl Event { impl Event {
fn event_type(&self) -> EventType { fn event_type(&self) -> EventType {
match self { match self {
Self::Pointer(_) => EventType::POINTER, Self::Pointer(_) => EventType::Pointer,
Self::Keyboard(_) => EventType::KEYBOARD, Self::Keyboard(_) => EventType::Keyboard,
Self::Release() => EventType::RELEASE, Self::Enter() => EventType::Enter,
Self::Ping() => EventType::PING, Self::Leave() => EventType::Leave,
Self::Pong() => EventType::PONG, Self::Ping() => EventType::Ping,
Self::Pong() => EventType::Pong,
} }
} }
} }
@@ -123,10 +140,10 @@ impl Event {
impl PointerEvent { impl PointerEvent {
fn event_type(&self) -> PointerEventType { fn event_type(&self) -> PointerEventType {
match self { match self {
Self::Motion { .. } => PointerEventType::MOTION, Self::Motion { .. } => PointerEventType::Motion,
Self::Button { .. } => PointerEventType::BUTTON, Self::Button { .. } => PointerEventType::Button,
Self::Axis { .. } => PointerEventType::AXIS, Self::Axis { .. } => PointerEventType::Axis,
Self::Frame { .. } => PointerEventType::FRAME, Self::Frame { .. } => PointerEventType::Frame,
} }
} }
} }
@@ -134,28 +151,29 @@ impl PointerEvent {
impl KeyboardEvent { impl KeyboardEvent {
fn event_type(&self) -> KeyboardEventType { fn event_type(&self) -> KeyboardEventType {
match self { match self {
KeyboardEvent::Key { .. } => KeyboardEventType::KEY, KeyboardEvent::Key { .. } => KeyboardEventType::Key,
KeyboardEvent::Modifiers { .. } => KeyboardEventType::MODIFIERS, KeyboardEvent::Modifiers { .. } => KeyboardEventType::Modifiers,
} }
} }
} }
enum PointerEventType { enum PointerEventType {
MOTION, Motion,
BUTTON, Button,
AXIS, Axis,
FRAME, Frame,
} }
enum KeyboardEventType { enum KeyboardEventType {
KEY, Key,
MODIFIERS, Modifiers,
} }
enum EventType { enum EventType {
POINTER, Pointer,
KEYBOARD, Keyboard,
RELEASE, Enter,
PING, Leave,
PONG, Ping,
Pong,
} }
impl TryFrom<u8> for PointerEventType { impl TryFrom<u8> for PointerEventType {
@@ -163,10 +181,10 @@ impl TryFrom<u8> for PointerEventType {
fn try_from(value: u8) -> Result<Self, Self::Error> { fn try_from(value: u8) -> Result<Self, Self::Error> {
match value { match value {
x if x == Self::MOTION as u8 => Ok(Self::MOTION), x if x == Self::Motion as u8 => Ok(Self::Motion),
x if x == Self::BUTTON as u8 => Ok(Self::BUTTON), x if x == Self::Button as u8 => Ok(Self::Button),
x if x == Self::AXIS as u8 => Ok(Self::AXIS), x if x == Self::Axis as u8 => Ok(Self::Axis),
x if x == Self::FRAME as u8 => Ok(Self::FRAME), x if x == Self::Frame as u8 => Ok(Self::Frame),
_ => Err(Box::new(ProtocolError { _ => Err(Box::new(ProtocolError {
msg: format!("invalid pointer event type {}", value), msg: format!("invalid pointer event type {}", value),
})), })),
@@ -179,8 +197,8 @@ impl TryFrom<u8> for KeyboardEventType {
fn try_from(value: u8) -> Result<Self, Self::Error> { fn try_from(value: u8) -> Result<Self, Self::Error> {
match value { match value {
x if x == Self::KEY as u8 => Ok(Self::KEY), x if x == Self::Key as u8 => Ok(Self::Key),
x if x == Self::MODIFIERS as u8 => Ok(Self::MODIFIERS), x if x == Self::Modifiers as u8 => Ok(Self::Modifiers),
_ => Err(Box::new(ProtocolError { _ => Err(Box::new(ProtocolError {
msg: format!("invalid keyboard event type {}", value), msg: format!("invalid keyboard event type {}", value),
})), })),
@@ -188,17 +206,18 @@ impl TryFrom<u8> for KeyboardEventType {
} }
} }
impl Into<Vec<u8>> for &Event { impl From<&Event> for Vec<u8> {
fn into(self) -> Vec<u8> { fn from(event: &Event) -> Self {
let event_id = vec![self.event_type() as u8]; let event_id = vec![event.event_type() as u8];
let event_data = match self { let event_data = match event {
Event::Pointer(p) => p.into(), Event::Pointer(p) => p.into(),
Event::Keyboard(k) => k.into(), Event::Keyboard(k) => k.into(),
Event::Release() => vec![], Event::Enter() => vec![],
Event::Leave() => vec![],
Event::Ping() => vec![], Event::Ping() => vec![],
Event::Pong() => vec![], Event::Pong() => vec![],
}; };
vec![event_id, event_data].concat() [event_id, event_data].concat()
} }
} }
@@ -220,11 +239,12 @@ impl TryFrom<Vec<u8>> for Event {
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> { fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
let event_id = u8::from_be_bytes(value[..1].try_into()?); let event_id = u8::from_be_bytes(value[..1].try_into()?);
match event_id { match event_id {
i if i == (EventType::POINTER as u8) => Ok(Event::Pointer(value.try_into()?)), 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::Keyboard as u8) => Ok(Event::Keyboard(value.try_into()?)),
i if i == (EventType::RELEASE as u8) => Ok(Event::Release()), i if i == (EventType::Enter as u8) => Ok(Event::Enter()),
i if i == (EventType::PING as u8) => Ok(Event::Ping()), i if i == (EventType::Leave as u8) => Ok(Event::Leave()),
i if i == (EventType::PONG as u8) => Ok(Event::Pong()), i if i == (EventType::Ping as u8) => Ok(Event::Ping()),
i if i == (EventType::Pong as u8) => Ok(Event::Pong()),
_ => Err(Box::new(ProtocolError { _ => Err(Box::new(ProtocolError {
msg: format!("invalid event_id {}", event_id), msg: format!("invalid event_id {}", event_id),
})), })),
@@ -232,10 +252,10 @@ impl TryFrom<Vec<u8>> for Event {
} }
} }
impl Into<Vec<u8>> for &PointerEvent { impl From<&PointerEvent> for Vec<u8> {
fn into(self) -> Vec<u8> { fn from(event: &PointerEvent) -> Self {
let id = vec![self.event_type() as u8]; let id = vec![event.event_type() as u8];
let data = match self { let data = match event {
PointerEvent::Motion { PointerEvent::Motion {
time, time,
relative_x, relative_x,
@@ -244,7 +264,7 @@ impl Into<Vec<u8>> for &PointerEvent {
let time = time.to_be_bytes(); let time = time.to_be_bytes();
let relative_x = relative_x.to_be_bytes(); let relative_x = relative_x.to_be_bytes();
let relative_y = relative_y.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 { PointerEvent::Button {
time, time,
@@ -254,19 +274,19 @@ impl Into<Vec<u8>> for &PointerEvent {
let time = time.to_be_bytes(); let time = time.to_be_bytes();
let button = button.to_be_bytes(); let button = button.to_be_bytes();
let state = state.to_be_bytes(); let state = state.to_be_bytes();
vec![&time[..], &button[..], &state[..]].concat() [&time[..], &button[..], &state[..]].concat()
} }
PointerEvent::Axis { time, axis, value } => { PointerEvent::Axis { time, axis, value } => {
let time = time.to_be_bytes(); let time = time.to_be_bytes();
let axis = axis.to_be_bytes(); let axis = axis.to_be_bytes();
let value = value.to_be_bytes(); let value = value.to_be_bytes();
vec![&time[..], &axis[..], &value[..]].concat() [&time[..], &axis[..], &value[..]].concat()
} }
PointerEvent::Frame {} => { PointerEvent::Frame {} => {
vec![] vec![]
} }
}; };
vec![id, data].concat() [id, data].concat()
} }
} }
@@ -281,7 +301,7 @@ impl TryFrom<Vec<u8>> for PointerEvent {
Err(e) => return Err(e), Err(e) => return Err(e),
}; };
match event_type { match event_type {
PointerEventType::MOTION => { PointerEventType::Motion => {
let time = match data.get(2..6) { let time = match data.get(2..6) {
Some(d) => u32::from_be_bytes(d.try_into()?), Some(d) => u32::from_be_bytes(d.try_into()?),
None => { None => {
@@ -312,7 +332,7 @@ impl TryFrom<Vec<u8>> for PointerEvent {
relative_y, relative_y,
}) })
} }
PointerEventType::BUTTON => { PointerEventType::Button => {
let time = match data.get(2..6) { let time = match data.get(2..6) {
Some(d) => u32::from_be_bytes(d.try_into()?), Some(d) => u32::from_be_bytes(d.try_into()?),
None => { None => {
@@ -343,7 +363,7 @@ impl TryFrom<Vec<u8>> for PointerEvent {
state, state,
}) })
} }
PointerEventType::AXIS => { PointerEventType::Axis => {
let time = match data.get(2..6) { let time = match data.get(2..6) {
Some(d) => u32::from_be_bytes(d.try_into()?), Some(d) => u32::from_be_bytes(d.try_into()?),
None => { None => {
@@ -370,7 +390,7 @@ impl TryFrom<Vec<u8>> for PointerEvent {
}; };
Ok(Self::Axis { time, axis, value }) Ok(Self::Axis { time, axis, value })
} }
PointerEventType::FRAME => Ok(Self::Frame {}), PointerEventType::Frame => Ok(Self::Frame {}),
} }
} }
None => Err(Box::new(ProtocolError { None => Err(Box::new(ProtocolError {
@@ -380,15 +400,15 @@ impl TryFrom<Vec<u8>> for PointerEvent {
} }
} }
impl Into<Vec<u8>> for &KeyboardEvent { impl From<&KeyboardEvent> for Vec<u8> {
fn into(self) -> Vec<u8> { fn from(event: &KeyboardEvent) -> Self {
let id = vec![self.event_type() as u8]; let id = vec![event.event_type() as u8];
let data = match self { let data = match event {
KeyboardEvent::Key { time, key, state } => { KeyboardEvent::Key { time, key, state } => {
let time = time.to_be_bytes(); let time = time.to_be_bytes();
let key = key.to_be_bytes(); let key = key.to_be_bytes();
let state = state.to_be_bytes(); let state = state.to_be_bytes();
vec![&time[..], &key[..], &state[..]].concat() [&time[..], &key[..], &state[..]].concat()
} }
KeyboardEvent::Modifiers { KeyboardEvent::Modifiers {
mods_depressed, mods_depressed,
@@ -400,7 +420,7 @@ impl Into<Vec<u8>> for &KeyboardEvent {
let mods_latched = mods_latched.to_be_bytes(); let mods_latched = mods_latched.to_be_bytes();
let mods_locked = mods_locked.to_be_bytes(); let mods_locked = mods_locked.to_be_bytes();
let group = group.to_be_bytes(); let group = group.to_be_bytes();
vec![ [
&mods_depressed[..], &mods_depressed[..],
&mods_latched[..], &mods_latched[..],
&mods_locked[..], &mods_locked[..],
@@ -409,7 +429,7 @@ impl Into<Vec<u8>> for &KeyboardEvent {
.concat() .concat()
} }
}; };
vec![id, data].concat() [id, data].concat()
} }
} }
@@ -424,7 +444,7 @@ impl TryFrom<Vec<u8>> for KeyboardEvent {
Err(e) => return Err(e), Err(e) => return Err(e),
}; };
match event_type { match event_type {
KeyboardEventType::KEY => { KeyboardEventType::Key => {
let time = match data.get(2..6) { let time = match data.get(2..6) {
Some(d) => u32::from_be_bytes(d.try_into()?), Some(d) => u32::from_be_bytes(d.try_into()?),
None => { None => {
@@ -451,7 +471,7 @@ impl TryFrom<Vec<u8>> for KeyboardEvent {
}; };
Ok(KeyboardEvent::Key { time, key, state }) Ok(KeyboardEvent::Key { time, key, state })
} }
KeyboardEventType::MODIFIERS => { KeyboardEventType::Modifiers => {
let mods_depressed = match data.get(2..6) { let mods_depressed = match data.get(2..6) {
Some(d) => u32::from_be_bytes(d.try_into()?), Some(d) => u32::from_be_bytes(d.try_into()?),
None => { None => {

View File

@@ -31,16 +31,16 @@ use crate::{
pub mod cli; pub mod cli;
/// gtk frontend /// gtk frontend
#[cfg(all(unix, feature = "gtk"))] #[cfg(feature = "gtk")]
pub mod gtk; pub mod gtk;
pub fn run_frontend(config: &Config) -> Result<()> { pub fn run_frontend(config: &Config) -> Result<()> {
match config.frontend { match config.frontend {
#[cfg(all(unix, feature = "gtk"))] #[cfg(feature = "gtk")]
Frontend::Gtk => { Frontend::Gtk => {
gtk::run(); gtk::run();
} }
#[cfg(any(not(feature = "gtk"), not(unix)))] #[cfg(not(feature = "gtk"))]
Frontend::Gtk => panic!("gtk frontend requested but feature not enabled!"), Frontend::Gtk => panic!("gtk frontend requested but feature not enabled!"),
Frontend::Cli => { Frontend::Cli => {
cli::run()?; cli::run()?;
@@ -201,8 +201,6 @@ impl FrontendListener {
#[cfg(unix)] #[cfg(unix)]
pub async fn accept(&mut self) -> Result<ReadHalf<UnixStream>> { pub async fn accept(&mut self) -> Result<ReadHalf<UnixStream>> {
log::trace!("frontend.accept()");
let stream = self.listener.accept().await?.0; let stream = self.listener.accept().await?.0;
let (rx, tx) = tokio::io::split(stream); let (rx, tx) = tokio::io::split(stream);
self.tx_streams.push(tx); self.tx_streams.push(tx);
@@ -229,11 +227,11 @@ impl FrontendListener {
// TODO do simultaneously // TODO do simultaneously
for tx in self.tx_streams.iter_mut() { for tx in self.tx_streams.iter_mut() {
// write len + payload // write len + payload
if let Err(_) = tx.write(&len).await { if tx.write(&len).await.is_err() {
keep.push(false); keep.push(false);
continue; continue;
} }
if let Err(_) = tx.write(payload).await { if tx.write(payload).await.is_err() {
keep.push(false); keep.push(false);
continue; continue;
} }

View File

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

View File

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

View File

@@ -63,7 +63,7 @@ impl ClientRow {
let port_binding = client_object let port_binding = client_object
.bind_property("port", &self.imp().port.get(), "text") .bind_property("port", &self.imp().port.get(), "text")
.transform_from(|_, v: String| { .transform_from(|_, v: String| {
if v == "" { if v.is_empty() {
Some(DEFAULT_PORT as u32) Some(DEFAULT_PORT as u32)
} else { } else {
Some(v.parse::<u16>().unwrap_or(DEFAULT_PORT) as u32) 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> { pub fn client_idx(&self, handle: ClientHandle) -> Option<usize> {
self.clients() self.clients().iter::<ClientObject>().position(|c| {
.iter::<ClientObject>() if let Ok(c) = c {
.position(|c| { c.handle() == handle
if let Ok(c) = c { } else {
c.handle() == handle false
} else { }
false })
}
})
.map(|p| p as usize)
} }
pub fn delete_client(&self, handle: ClientHandle) { pub fn delete_client(&self, handle: ClientHandle) {
@@ -117,7 +114,7 @@ impl Window {
pub fn request_port_change(&self) { pub fn request_port_change(&self) {
let port = self.imp().port_entry.get().text().to_string(); 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)); self.request(FrontendEvent::ChangePort(port));
} else { } else {
self.request(FrontendEvent::ChangePort(DEFAULT_PORT)); self.request(FrontendEvent::ChangePort(DEFAULT_PORT));

View File

@@ -1,7 +1,9 @@
use std::{ use std::cell::{Cell, RefCell};
cell::{Cell, RefCell},
os::unix::net::UnixStream, #[cfg(windows)]
}; use std::net::TcpStream;
#[cfg(unix)]
use std::os::unix::net::UnixStream;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use adw::{ use adw::{
@@ -29,7 +31,10 @@ pub struct Window {
#[template_child] #[template_child]
pub toast_overlay: TemplateChild<ToastOverlay>, pub toast_overlay: TemplateChild<ToastOverlay>,
pub clients: RefCell<Option<gio::ListStore>>, pub clients: RefCell<Option<gio::ListStore>>,
#[cfg(unix)]
pub stream: RefCell<Option<UnixStream>>, pub stream: RefCell<Option<UnixStream>>,
#[cfg(windows)]
pub stream: RefCell<Option<TcpStream>>,
pub port: Cell<u16>, 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 backend;
pub mod frontend; 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 std::process::{self, Child, Command};
use env_logger::Env; use env_logger::Env;
use lan_mouse::{ use lan_mouse::{config::Config, frontend, server::Server};
config::Config,
consumer,
frontend::{self, FrontendListener},
producer,
server::Server,
};
use tokio::{join, task::LocalSet}; use tokio::task::LocalSet;
pub fn main() { pub fn main() {
// init logging // init logging
@@ -42,8 +36,19 @@ pub fn run() -> Result<()> {
} else { } else {
// otherwise start the service as a child process and // otherwise start the service as a child process and
// run a frontend // run a frontend
start_service()?; let mut service = start_service()?;
frontend::run_frontend(&config)?; 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(()) anyhow::Ok(())
@@ -58,27 +63,10 @@ fn run_service(config: &Config) -> Result<()> {
// run async event loop // run async event loop
runtime.block_on(LocalSet::new().run_until(async { runtime.block_on(LocalSet::new().run_until(async {
// create frontend communication adapter // run main loop
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?;
log::info!("Press Ctrl+Alt+Shift+Super to release the mouse"); log::info!("Press Ctrl+Alt+Shift+Super to release the mouse");
Server::run(config).await?;
// run event loop
event_server.run().await?;
log::debug!("service exiting"); log::debug!("service exiting");
anyhow::Ok(()) anyhow::Ok(())
}))?; }))?;

View File

@@ -1,4 +1,3 @@
use anyhow::Result;
use std::io; use std::io;
use futures_core::Stream; use futures_core::Stream;
@@ -9,77 +8,48 @@ use crate::{
event::Event, event::Event,
}; };
#[cfg(all(unix, not(target_os = "macos")))] pub async fn create() -> Box<dyn EventProducer> {
use std::env;
#[cfg(all(unix, not(target_os = "macos")))]
enum Backend {
LayerShell,
Libei,
X11,
}
pub async fn create() -> Result<Box<dyn EventProducer>> {
#[cfg(target_os = "macos")] #[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)] #[cfg(windows)]
return Ok(Box::new(producer::windows::WindowsProducer::new())); match producer::windows::WindowsProducer::new() {
Ok(p) => return Box::new(p),
#[cfg(all(unix, not(target_os = "macos")))] Err(e) => log::info!("windows event producer not available: {e}"),
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()?))
}
} }
#[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 { pub trait EventProducer: Stream<Item = io::Result<(ClientHandle, Event)>> + Unpin {

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