Compare commits

..

111 Commits

Author SHA1 Message Date
Ferdinand Schober
0119f8870e update screenshots 2024-11-09 14:03:25 +01:00
Ferdinand Schober
8b0a797d75 update connection instructions 2024-11-09 13:40:04 +01:00
Ferdinand Schober
3913fa8a38 update README 2024-11-09 12:35:45 +01:00
Ferdinand Schober
682a7b65bd cleanup 2024-11-09 12:03:50 +01:00
Ferdinand Schober
ae58714681 fix initial activation 2024-11-08 23:03:12 +01:00
Ferdinand Schober
875a31907a cleanup 2024-11-08 22:45:11 +01:00
Ferdinand Schober
6cff691a38 reduce visibility of functions 2024-11-08 21:27:51 +01:00
Ferdinand Schober
f9217fe268 simplify service 2024-11-08 18:45:38 +01:00
Ferdinand Schober
809fbbf453 simplify Service 2024-11-08 18:40:10 +01:00
Ferdinand Schober
6dfd5d97a4 remove dependency on service 2024-11-08 18:33:47 +01:00
Ferdinand Schober
156be7a79d remove unnecessary struct 2024-11-08 18:25:26 +01:00
Ferdinand Schober
7d6a08141c fix initial capture creation 2024-11-08 18:04:29 +01:00
Ferdinand Schober
37d35d3eea remove inactive emulation handles 2024-11-08 17:53:48 +01:00
Ferdinand Schober
2be9fbe2a4 cleanup emulation 2024-11-08 17:49:47 +01:00
Ferdinand Schober
9837e3d9d2 cleanup 2024-11-08 17:33:28 +01:00
Ferdinand Schober
d30bd81fa9 simplify 2024-11-08 17:20:32 +01:00
Ferdinand Schober
ef2b2a773e remove dependency on service from capture 2024-11-08 17:18:51 +01:00
Ferdinand Schober
46044d0796 no need for a Cell 2024-11-08 14:07:34 +01:00
Ferdinand Schober
ff2a2cb1df move enter hook logic to service (where it belongs) 2024-11-08 13:59:38 +01:00
Ferdinand Schober
88693093c6 revert to 1px 2024-11-08 13:59:20 +01:00
Ferdinand Schober
1f1d3f2ccf properly terminate input capture 2024-11-07 21:21:06 +01:00
Ferdinand Schober
60fab82423 fix update pos 2024-11-07 21:21:06 +01:00
Ferdinand Schober
4d835a5190 fix potential unterminated emulation session 2024-11-07 21:21:06 +01:00
Ferdinand Schober
707cef154e cleanup 2024-11-07 21:21:06 +01:00
Ferdinand Schober
6bc00bd84d terminate capture in all instances 2024-11-07 21:21:06 +01:00
Ferdinand Schober
bad8c5a81b update pos when entered from a different side 2024-11-07 21:21:06 +01:00
Ferdinand Schober
b73c5991ad ignore port change if same port 2024-11-07 21:21:06 +01:00
Ferdinand Schober
39d5d1c03b clippy 2024-11-07 21:21:06 +01:00
Ferdinand Schober
b94a453703 use a guard struct to send enable / disable events 2024-11-07 21:21:06 +01:00
Ferdinand Schober
9a4206dab7 simplify logic 2024-11-07 21:21:06 +01:00
Ferdinand Schober
677f662f4f fix disconnect 2024-11-07 21:21:06 +01:00
Ferdinand Schober
1d45d14992 fix clippy lint 2024-11-07 21:21:06 +01:00
Ferdinand Schober
b87fe4164a remove key-gen script 2024-11-07 21:21:06 +01:00
Ferdinand Schober
151fbe37de fix capture termination when in client creation 2024-11-07 21:21:06 +01:00
Ferdinand Schober
095937e943 fix service exit 2024-11-07 21:21:06 +01:00
Ferdinand Schober
5461c6a00e resolve dns when activating 2024-11-07 21:21:06 +01:00
Ferdinand Schober
aa4097b4ee restructure resolver + capture 2024-11-07 21:21:06 +01:00
Ferdinand Schober
1489719e01 handle port change 2024-11-07 21:21:06 +01:00
Ferdinand Schober
2291cf25a3 improve error handling 2024-11-07 21:21:06 +01:00
Ferdinand Schober
a5727801e3 fix connection logic
dont abort after first ip,
add timeout logic
2024-11-07 21:21:06 +01:00
Ferdinand Schober
92398db918 update log msg 2024-11-07 21:21:06 +01:00
Ferdinand Schober
847826e8ba create config directory if it does not exist 2024-11-07 21:21:06 +01:00
Ferdinand Schober
f62cd3d11c (hopefully) fix ping logic 2024-11-07 21:21:06 +01:00
Ferdinand Schober
9dc4e95a4d avoid duplicating incoming handles 2024-11-07 21:21:06 +01:00
Ferdinand Schober
87304744ad fix release logic 2024-11-07 21:21:06 +01:00
Ferdinand Schober
0277b6b4d3 add release capture log message 2024-11-07 21:21:06 +01:00
Ferdinand Schober
b6c2cfe8a6 fix clippy 2024-11-07 21:21:06 +01:00
Ferdinand Schober
4a64a97273 release capture only if no active capture at pos 2024-11-07 21:21:06 +01:00
Ferdinand Schober
06d4e8d836 implementing release logic for one-way connection 2024-11-07 21:21:06 +01:00
Ferdinand Schober
44e34918bb fix config path in windows 2024-11-07 21:21:06 +01:00
Ferdinand Schober
52c4e1cd7f silence server_name warning 2024-11-07 21:21:06 +01:00
Ferdinand Schober
a39f9172d5 load authorized keys from config 2024-11-07 21:21:06 +01:00
Ferdinand Schober
c205371dfc [wip] one-way control 2024-11-07 21:21:06 +01:00
Ferdinand Schober
81ca510d12 put hostname + port into one row 2024-11-07 21:21:06 +01:00
Ferdinand Schober
4b6a7d5410 fix tooltip text 2024-11-07 21:21:06 +01:00
Ferdinand Schober
1979f203d2 release capture if emulation disabled on target 2024-11-07 21:21:06 +01:00
Ferdinand Schober
f6a7010d17 merge incoming conns and "authorized" section 2024-11-07 21:21:06 +01:00
Ferdinand Schober
cf4a06a44a make private key file inaccessible to other users 2024-11-07 21:21:06 +01:00
Ferdinand Schober
49e139198a remove log messages 2024-11-07 21:21:06 +01:00
Ferdinand Schober
d7c8d79b94 fix ui reversed 2024-11-07 21:21:06 +01:00
Ferdinand Schober
2ed3624505 fix fingerprint add 2024-11-07 21:21:06 +01:00
Ferdinand Schober
c75a085e78 use certificate when connecting 2024-11-07 21:21:06 +01:00
Ferdinand Schober
790e1b927f gen or load certificate 2024-11-07 21:21:06 +01:00
Ferdinand Schober
61d4a6ceac enable client cert auth 2024-11-07 21:21:06 +01:00
Ferdinand Schober
dedf59d1c7 add logic for fingerprint verification 2024-11-07 21:21:06 +01:00
Ferdinand Schober
1c7490c58d impl fingerprint ui logic 2024-11-07 21:21:06 +01:00
Ferdinand Schober
0038178f0d fingerprint add ui mockup 2024-11-07 21:21:06 +01:00
Ferdinand Schober
f056f790c7 add ui mockup 2024-11-07 21:21:06 +01:00
Ferdinand Schober
c480bb6ea6 move cert verification to server 2024-11-07 21:21:06 +01:00
Ferdinand Schober
94ece6dfe6 verify_peer_certificate fn 2024-11-07 21:21:06 +01:00
Ferdinand Schober
44707bd786 update logs 2024-11-07 21:21:06 +01:00
Ferdinand Schober
640447ecaa fingerprints 2024-11-07 21:21:06 +01:00
Ferdinand Schober
859bec5df8 remove some debug logs 2024-11-07 21:21:06 +01:00
Ferdinand Schober
ed7771691f dont activate multiple times 2024-11-07 21:21:06 +01:00
Ferdinand Schober
b16f3b272a fix double release 2024-11-07 21:21:06 +01:00
Ferdinand Schober
fa63a7e9e5 fix active 2024-11-07 21:21:06 +01:00
Ferdinand Schober
f825f3be53 fix ping logic 2024-11-07 21:21:06 +01:00
Ferdinand Schober
22bf7dc8c2 use addr from accept 2024-11-07 21:21:06 +01:00
Ferdinand Schober
4bbabf1e0a fix compilation 2024-11-07 21:21:06 +01:00
Ferdinand Schober
64c4480e93 enter acknowledgement 2024-11-07 21:21:06 +01:00
Ferdinand Schober
b41ee94a2b hotfix accept bug
https://github.com/webrtc-rs/webrtc/issues/614
2024-11-07 21:21:06 +01:00
Ferdinand Schober
3e62739f7e debug connections 2024-11-07 21:21:06 +01:00
Ferdinand Schober
9593b97fbe formatting 2024-11-07 21:21:06 +01:00
Ferdinand Schober
ae5f3e5303 remove unnecessary let 2024-11-07 21:21:06 +01:00
Ferdinand Schober
eb0022995c fix deadlock 2024-11-07 21:21:06 +01:00
Ferdinand Schober
874fba670d fix 2024-11-07 21:21:06 +01:00
Ferdinand Schober
fd5b8ed25e release capture 2024-11-07 21:21:06 +01:00
Ferdinand Schober
bc192bdf6b ping server 2024-11-07 21:21:06 +01:00
Ferdinand Schober
b747c7252b prevent releasing keys logspam 2024-11-07 21:21:06 +01:00
Ferdinand Schober
89a4672d36 fix clippy 2024-11-07 21:21:06 +01:00
Ferdinand Schober
89ab328fc9 fix termination 2024-11-07 21:21:06 +01:00
Ferdinand Schober
b710fe273f fix comment 2024-11-07 21:21:06 +01:00
Ferdinand Schober
e2479762ab fix capture reenable 2024-11-07 21:21:06 +01:00
Ferdinand Schober
2c9ecf98d0 update log message 2024-11-07 21:21:06 +01:00
Ferdinand Schober
62de35de9d fix connection logic 2024-11-07 21:21:06 +01:00
Ferdinand Schober
7baacf4546 debounce release log 2024-11-07 21:21:06 +01:00
Ferdinand Schober
7f0815facf improve logging, remove failed connection 2024-11-07 21:21:06 +01:00
Ferdinand Schober
7186ae40b1 resolver: encapsulate channel logic 2024-11-07 21:21:06 +01:00
Ferdinand Schober
0bf0173971 add release bind check 2024-11-07 21:21:06 +01:00
Ferdinand Schober
17f3d7237c remove unused error 2024-11-07 21:21:06 +01:00
Ferdinand Schober
e427439099 connect asynchronously 2024-11-07 21:21:06 +01:00
Ferdinand Schober
0fe5416ac5 dont use remote_addr 2024-11-07 21:21:06 +01:00
Ferdinand Schober
937652ac44 finish up server 2024-11-07 21:21:06 +01:00
Ferdinand Schober
ad8c92cfbe capture stuffs 2024-11-07 21:21:06 +01:00
Ferdinand Schober
e4a7f0b4fc connection things 2024-11-07 21:21:06 +01:00
Ferdinand Schober
28e4895418 impl emulation stuff 2024-11-07 21:21:06 +01:00
Ferdinand Schober
b62540d9be asdf 2024-11-07 21:21:06 +01:00
Ferdinand Schober
90561744a0 asdf 2024-11-07 21:21:06 +01:00
Ferdinand Schober
4c000fa6d4 test fix port 2024-11-07 21:21:06 +01:00
Ferdinand Schober
e7166d4373 test selfsigned 2024-11-07 21:21:06 +01:00
Ferdinand Schober
79bc64e56e start working on encryption 2024-11-07 21:21:06 +01:00
62 changed files with 2903 additions and 4156 deletions

View File

@@ -84,60 +84,36 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: install dependencies
run: brew install gtk4 libadwaita imagemagick
run: brew install gtk4 libadwaita
- name: Release Build
run: |
cargo build --release
cp target/release/lan-mouse lan-mouse-macos-intel
- name: Make icns
run: scripts/makeicns.sh
- name: Install cargo bundle
run: cargo install cargo-bundle
- name: Bundle
run: |
cargo bundle --release
scripts/copy-macos-dylib.sh "target/release/bundle/osx/Lan Mouse.app/Contents/MacOS/lan-mouse"
- name: Zip bundle
run: |
cd target/release/bundle/osx
zip -r "lan-mouse-macos-intel.zip" "Lan Mouse.app"
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: lan-mouse-macos-intel
path: target/release/bundle/osx/lan-mouse-macos-intel.zip
path: lan-mouse-macos-intel
macos-aarch64-release-build:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: install dependencies
run: brew install gtk4 libadwaita imagemagick
run: brew install gtk4 libadwaita
- name: Release Build
run: |
cargo build --release
cp target/release/lan-mouse lan-mouse-macos-aarch64
- name: Make icns
run: scripts/makeicns.sh
- name: Install cargo bundle
run: cargo install cargo-bundle
- name: Bundle
run: |
cargo bundle --release
scripts/copy-macos-dylib.sh "target/release/bundle/osx/Lan Mouse.app/Contents/MacOS/lan-mouse"
- name: Zip bundle
run: |
cd target/release/bundle/osx
zip -r "lan-mouse-macos-aarch64.zip" "Lan Mouse.app"
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: lan-mouse-macos-aarch64
path: target/release/bundle/osx/lan-mouse-macos-aarch64.zip
path: lan-mouse-macos-aarch64
pre-release:
name: "Pre Release"
needs: [windows-release-build, linux-release-build, macos-release-build, macos-aarch64-release-build]
needs: [windows-release-build, linux-release-build, macos-release-build]
runs-on: "ubuntu-latest"
steps:
- name: Download build artifacts
@@ -151,6 +127,6 @@ jobs:
title: "Development Build"
files: |
lan-mouse-linux/lan-mouse
lan-mouse-macos-intel/lan-mouse-macos-intel.zip
lan-mouse-macos-aarch64/lan-mouse-macos-aarch64.zip
lan-mouse-macos-intel/lan-mouse-macos-intel
lan-mouse-macos-aarch64/lan-mouse-macos-aarch64
lan-mouse-windows/lan-mouse-windows.zip

View File

@@ -98,7 +98,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: install dependencies
run: brew install gtk4 libadwaita imagemagick
run: brew install gtk4 libadwaita
- name: Build
run: cargo build --verbose
- name: Run tests
@@ -107,30 +107,18 @@ jobs:
run: cargo fmt --check
- name: Clippy
run: cargo clippy --all-features --all-targets -- --deny warnings
- name: Make icns
run: scripts/makeicns.sh
- name: Install cargo bundle
run: cargo install cargo-bundle
- name: Bundle
run: |
cargo bundle
scripts/copy-macos-dylib.sh
- name: Zip bundle
run: |
cd target/debug/bundle/osx
zip -r "Lan Mouse macOS (Intel).zip" "Lan Mouse.app"
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: Lan Mouse macOS (Intel)
path: target/debug/bundle/osx/Lan Mouse macOS (Intel).zip
name: lan-mouse-macos
path: target/debug/lan-mouse
build-macos-aarch64:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: install dependencies
run: brew install gtk4 libadwaita imagemagick
run: brew install gtk4 libadwaita
- name: Build
run: cargo build --verbose
- name: Run tests
@@ -139,20 +127,8 @@ jobs:
run: cargo fmt --check
- name: Clippy
run: cargo clippy --all-features --all-targets -- --deny warnings
- name: Make icns
run: scripts/makeicns.sh
- name: Install cargo bundle
run: cargo install cargo-bundle
- name: Bundle
run: |
cargo bundle
scripts/copy-macos-dylib.sh
- name: Zip bundle
run: |
cd target/debug/bundle/osx
zip -r "Lan Mouse macOS (ARM).zip" "Lan Mouse.app"
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: Lan Mouse macOS (ARM)
path: target/debug/bundle/osx/Lan Mouse macOS (ARM).zip
name: lan-mouse-macos-aarch64
path: target/debug/lan-mouse

View File

@@ -80,60 +80,36 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: install dependencies
run: brew install gtk4 libadwaita imagemagick
run: brew install gtk4 libadwaita
- name: Release Build
run: |
cargo build --release
cp target/release/lan-mouse lan-mouse-macos-intel
- name: Make icns
run: scripts/makeicns.sh
- name: Install cargo bundle
run: cargo install cargo-bundle
- name: Bundle
run: |
cargo bundle --release
scripts/copy-macos-dylib.sh "target/release/bundle/osx/Lan Mouse.app/Contents/MacOS/lan-mouse"
- name: Zip bundle
run: |
cd target/release/bundle/osx
zip -r "lan-mouse-macos-intel.zip" "Lan Mouse.app"
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: lan-mouse-macos-intel.zip
path: target/release/bundle/osx/lan-mouse-macos-intel.zip
name: lan-mouse-macos-intel
path: lan-mouse-macos-intel
macos-aarch64-release-build:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: install dependencies
run: brew install gtk4 libadwaita imagemagick
run: brew install gtk4 libadwaita
- name: Release Build
run: |
cargo build --release
cp target/release/lan-mouse lan-mouse-macos-aarch64
- name: Make icns
run: scripts/makeicns.sh
- name: Install cargo bundle
run: cargo install cargo-bundle
- name: Bundle
run: |
cargo bundle --release
scripts/copy-macos-dylib.sh "target/release/bundle/osx/Lan Mouse.app/Contents/MacOS/lan-mouse"
- name: Zip bundle
run: |
cd target/release/bundle/osx
zip -r "lan-mouse-macos-aarch64.zip" "Lan Mouse.app"
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: lan-mouse-macos-aarch64.zip
path: target/release/bundle/osx/lan-mouse-macos-aarch64.zip
name: lan-mouse-macos-aarch64
path: lan-mouse-macos-aarch64
tagged-release:
name: "Tagged Release"
needs: [windows-release-build, linux-release-build, macos-release-build, macos-aarch64-release-build]
needs: [windows-release-build, linux-release-build, macos-release-build]
runs-on: "ubuntu-latest"
steps:
- name: Download build artifacts
@@ -145,6 +121,6 @@ jobs:
prerelease: false
files: |
lan-mouse-linux/lan-mouse
lan-mouse-macos-intel/lan-mouse-macos-intel.zip
lan-mouse-macos-aarch64/lan-mouse-macos-aarch64.zip
lan-mouse-macos-intel/lan-mouse-macos-intel
lan-mouse-macos-aarch64/lan-mouse-macos-aarch64
lan-mouse-windows/lan-mouse-windows.zip

4
.gitignore vendored
View File

@@ -8,7 +8,3 @@ result
*.pem
*.csr
extfile.conf
# flatpak files
.flatpak-builder
repo

2233
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,9 +23,6 @@ lto = "fat"
strip = true
panic = "abort"
[build-dependencies]
shadow-rs = "1.2.0"
[dependencies]
input-event = { path = "input-event", version = "0.3.0" }
input-emulation = { path = "input-emulation", version = "0.3.0", default-features = false }
@@ -34,9 +31,8 @@ lan-mouse-cli = { path = "lan-mouse-cli", version = "0.2.0" }
lan-mouse-gtk = { path = "lan-mouse-gtk", version = "0.2.0", optional = true }
lan-mouse-ipc = { path = "lan-mouse-ipc", version = "0.2.0" }
lan-mouse-proto = { path = "lan-mouse-proto", version = "0.2.0" }
shadow-rs = { version = "1.2.0", features = ["metadata"] }
hickory-resolver = "0.25.2"
hickory-resolver = "0.24.1"
toml = "0.8"
serde = { version = "1.0", features = ["derive"] }
log = "0.4.20"
@@ -58,8 +54,8 @@ slab = "0.4.9"
thiserror = "2.0.0"
tokio-util = "0.7.11"
local-channel = "0.1.5"
webrtc-dtls = { version = "0.12.0", features = ["pem"] }
webrtc-util = "0.11.0"
webrtc-dtls = { version = "0.10.0", features = ["pem"] }
webrtc-util = "0.9.0"
rustls = { version = "0.23.12", default-features = false, features = [
"std",
"ring",
@@ -89,8 +85,3 @@ libei_emulation = ["input-event/libei", "input-emulation/libei"]
wlroots_emulation = ["input-emulation/wlroots"]
x11_emulation = ["input-emulation/x11"]
rdp_emulation = ["input-emulation/remote_desktop_portal"]
[package.metadata.bundle]
name = "Lan Mouse"
icon = ["target/icon.icns"]
identifier = "de.feschber.LanMouse"

117
README.md
View File

@@ -50,10 +50,6 @@ Most current desktop environments and operating systems are fully supported, thi
For more detailed information about os support see [Detailed OS Support](#detailed-os-support)
### Android & IOS
A proof of concept for an Android / IOS Application by [rohitsangwan01](https://github.com/rohitsangwan01) can be found [here](https://github.com/rohitsangwan01/lan-mouse-mobile).
It can be used as a remote control for any device supported by Lan Mouse.
## Installation
@@ -81,37 +77,15 @@ paru -S lan-mouse-git
- flake: [README.md](./nix/README.md)
</details>
<details>
<summary>Fedora</summary>
You can install Lan Mouse from the [Terra Repository](https://terra.fyralabs.com).
After enabling Terra:
```sh
dnf install lan-mouse
```
</details>
<details>
<summary>MacOS</summary>
- Download the package for your Mac (Intel or ARM) from the releases page
- Unzip it
- Remove the quarantine with `xattr -rd com.apple.quarantine "Lan Mouse.app"`
- Launch the app
- Grant accessibility permissions in System Preferences
</details>
<details>
<summary>Manual Installation</summary>
First make sure to [install the necessary dependencies](#installing-dependencies-for-development--compiling-from-source).
First make sure to [install the necessary dependencies](#installing-dependencies).
Precompiled release binaries for Windows, MacOS and Linux are available in the [releases section](https://github.com/feschber/lan-mouse/releases).
For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies-for-development--compiling-from-source).
For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies).
Alternatively, the `lan-mouse` binary can be compiled from source (see below).
@@ -166,11 +140,10 @@ rust toolchain.
Additionally, available backends and frontends can be configured manually via
[cargo features](https://doc.rust-lang.org/cargo/reference/features.html).
E.g. if only support for sway is needed, the following command produces
an executable with support for only the `layer-shell` capture backend
and `wlroots` emulation backend:
E.g. if only wayland support is needed, the following command produces
an executable with just support for wayland:
```sh
cargo build --no-default-features --features layer_shell_capture,wlroots_emulation
cargo build --no-default-features --features wayland
```
For a detailed list of available features, checkout the [Cargo.toml](./Cargo.toml)
</details>
@@ -183,15 +156,7 @@ For a detailed list of available features, checkout the [Cargo.toml](./Cargo.tom
<summary>MacOS</summary>
```sh
# Install dependencies
brew install libadwaita pkg-config imagemagick
cargo install cargo-bundle
# Create the macOS icon file
scripts/makeicns.sh
# Create the .app bundle
cargo bundle
# Copy all dynamic libraries into the bundle, and update the bundle to find them there
scripts/copy-macos-dylib.sh
brew install libadwaita pkg-config
```
</details>
@@ -298,17 +263,19 @@ If the device still can not be entered, make sure you have UDP port `4242` (or t
<details>
<summary>Command Line Interface</summary>
The cli interface can be accessed by passing `cli` as a commandline argument.
Use
```sh
lan-mouse cli help
```
to list the available commands and
```sh
lan-mouse cli <cmd> help
```
for information on how to use a specific command.
The cli interface can be enabled using `--frontend cli` as commandline arguments.
Type `help` to list the available commands.
E.g.:
```sh
$ cargo run --release -- --frontend cli
(...)
> connect <host> left|right|top|bottom
(...)
> list
(...)
> activate 0
```
</details>
<details>
@@ -316,10 +283,10 @@ for information on how to use a specific command.
Lan Mouse can be launched in daemon mode to keep it running in the background (e.g. for use in a systemd-service).
To do so, use the `daemon` subcommand:
To do so, add `--daemon` to the commandline args:
```sh
lan-mouse daemon
lan-mouse --daemon
```
In order to start lan-mouse with a graphical session automatically,
@@ -343,7 +310,7 @@ To create this file you can copy the following example config:
### Example config
> [!TIP]
> key symbols in the release bind are named according
> to their names in [input-event/src/scancode.rs#L172](input-event/src/scancode.rs#L176).
> to their names in [src/scancode.rs#L172](src/scancode.rs#L172).
> This is bound to change
```toml
@@ -354,6 +321,9 @@ release_bind = [ "KeyA", "KeyS", "KeyD", "KeyF" ]
# optional port (defaults to 4242)
port = 4242
# # optional frontend -> defaults to gtk if available
# # possible values are "cli" and "gtk"
# frontend = "gtk"
# list of authorized tls certificate fingerprints that
# are accepted for incoming traffic
@@ -361,9 +331,7 @@ port = 4242
"bc:05:ab:7a:a4:de:88:8c:2f:92:ac:bc:b8:49:b8:24:0d:44:b3:e6:a4:ef:d7:0b:6c:69:6d:77:53:0b:14:80" = "iridium"
# define a client on the right side with host name "iridium"
[[clients]]
# position (left | right | top | bottom)
position = "right"
[right]
# hostname
hostname = "iridium"
# activate this client immediately when lan-mouse is started
@@ -372,8 +340,7 @@ activate_on_startup = true
ips = ["192.168.178.156"]
# define a client on the left side with IP address 192.168.178.189
[[clients]]
position = "left"
[left]
# The hostname is optional: When no hostname is specified,
# at least one ip address needs to be specified.
hostname = "thorium"
@@ -413,14 +380,14 @@ The following sections detail the emulation and capture backends provided by lan
### Input Emulation Support
| Desktop / Backend | wlroots | libei | remote-desktop portal | windows | macos | x11 |
|---------------------------|--------------------------|--------------------------|--------------------------|--------------------------|----------------------------------------|--------------------|
| Wayland (wlroots) | :heavy_check_mark: | | | | | |
| Wayland (KDE) | | :heavy_check_mark: | :heavy_check_mark: | | | |
| Wayland (Gnome) | | :heavy_check_mark: | :heavy_check_mark: | | | |
| Windows | | | | :heavy_check_mark: | | |
| MacOS | | | | | :heavy_check_mark: | |
| X11 | | | | | | :heavy_check_mark: |
| Desktop / Backend | layer-shell | libei | windows | macos | x11 |
|---------------------------|--------------------------|--------------------------|--------------------------|----------------------------------------|-----|
| Wayland (wlroots) | :heavy_check_mark: | | | | |
| Wayland (KDE) | :heavy_check_mark: | :heavy_check_mark: | | | |
| Wayland (Gnome) | | :heavy_check_mark: | | | |
| Windows | | | :heavy_check_mark: | | |
| MacOS | | | | :heavy_check_mark: | |
| X11 | | | | | WIP |
- `wlroots`: This backend makes use of the [wlr-virtual-pointer-unstable-v1](https://wayland.app/protocols/wlr-virtual-pointer-unstable-v1) and [virtual-keyboard-unstable-v1](https://wayland.app/protocols/virtual-keyboard-unstable-v1) protocols and is supported by most wlroots based compositors.
- `libei`: This backend uses [libei](https://gitlab.freedesktop.org/libinput/libei) and is supported by GNOME >= 45 or KDE Plasma >= 6.1.
@@ -433,14 +400,14 @@ The following sections detail the emulation and capture backends provided by lan
### Input Capture Support
| Desktop / Backend | layer-shell | libei | windows | macos | x11 |
|---------------------------|--------------------------|--------------------------|--------------------------|----------------------------------------|-----|
| Wayland (wlroots) | :heavy_check_mark: | | | | |
| Wayland (KDE) | :heavy_check_mark: | :heavy_check_mark: | | | |
| Wayland (Gnome) | | :heavy_check_mark: | | | |
| Windows | | | :heavy_check_mark: | | |
| MacOS | | | | :heavy_check_mark: | |
| X11 | | | | | WIP |
| Desktop / Backend | wlroots | libei | remote-desktop portal | windows | macos | x11 |
|---------------------------|--------------------------|--------------------------|--------------------------|--------------------------|----------------------------------------|--------------------|
| Wayland (wlroots) | :heavy_check_mark: | | | | | |
| Wayland (KDE) | | :heavy_check_mark: | :heavy_check_mark: | | | |
| Wayland (Gnome) | | :heavy_check_mark: | :heavy_check_mark: | | | |
| Windows | | | | :heavy_check_mark: | | |
| MacOS | | | | | :heavy_check_mark: | |
| X11 | | | | | | :heavy_check_mark: |
- `layer-shell`: This backend creates a single pixel wide window on the edges of Displays to capture the cursor using the [layer-shell protocol](https://wayland.app/protocols/wlr-layer-shell-unstable-v1).
- `libei`: This backend uses [libei](https://gitlab.freedesktop.org/libinput/libei) and is supported by GNOME >= 45 or KDE Plasma >= 6.1.

View File

@@ -1,50 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/flatpak/flatpak-builder/refs/heads/main/data/flatpak-manifest.schema.json
app-id: de.feschber.LanMouse
runtime: org.gnome.Platform
runtime-version: "48"
sdk: org.gnome.Sdk
sdk-extensions:
- org.freedesktop.Sdk.Extension.rust-stable
- org.freedesktop.Sdk.Extension.llvm20
command: /app/bin/lan-mouse
build-options:
append-path: "/usr/lib/sdk/rust-stable/bin:/usr/lib/sdk/llvm20/bin"
env:
"CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER": "clang"
"CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS": "-C link-arg=-fuse-ld=/usr/lib/sdk/rust-stable/bin/mold"
"CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER": "clang"
"CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS": "-C link-arg=-fuse-ld=/usr/lib/sdk/rust-stable/bin/mold"
build-args:
"--share=network"
prepend-ld-library-path:
"/usr/lib/sdk/llvm19/lib"
finish-args:
- "--socket=wayland"
- "--socket=fallback-x11"
- "--device=dri"
- "--socket=session-bus"
- "--share=network"
- "--filesystem=xdg-config"
- "--env=RUST_BACKTRACE=1"
- "--env=RUST_LOG=lan-mouse=debug"
- "--env=GTK_PATH=/app/lib/gtk-4.0"
modules:
- name: lan-mouse
buildsystem: simple
build-options:
build-args:
- "--share=network"
append-path: /usr/lib/sdk/rust-stable/bin
env:
CARGO_HOME: /run/build/lan-mouse/cargo
build-commands:
- cargo fetch --manifest-path Cargo.toml --verbose
- cargo build
- install -Dm0755 target/debug/lan-mouse /app/bin/lan-mouse
- install -Dm0644 lan-mouse-gtk/resources/de.feschber.LanMouse.svg ${FLATPAK_DEST}/share/icons/hicolor/scalable/apps/${FLATPAK_ID}.svg
- install -Dm0644 de.feschber.LanMouse.desktop ${FLATPAK_DEST}/share/applications/${FLATPAK_ID}.desktop
sources:
- type: dir
path: ..

View File

@@ -1,8 +1,15 @@
use shadow_rs::ShadowBuilder;
use std::process::Command;
fn main() {
ShadowBuilder::builder()
.deny_const(Default::default())
.build()
.expect("shadow build");
// commit hash
let git_describe = Command::new("git")
.arg("describe")
.arg("--always")
.arg("--dirty")
.arg("--tags")
.output()
.unwrap();
let git_describe = String::from_utf8(git_describe.stdout).unwrap();
println!("cargo::rustc-env=GIT_DESCRIBE={git_describe}");
}

View File

@@ -1,10 +1,14 @@
# example configuration
# configure release bind
release_bind = [ "KeyA", "KeyS", "KeyD", "KeyF" ]
# capture_backend = "LayerShell"
# release bind
release_bind = ["KeyA", "KeyS", "KeyD", "KeyF"]
# optional port (defaults to 4242)
port = 4242
# optional frontend -> defaults to gtk if available
# frontend = "gtk"
# list of authorized tls certificate fingerprints that
# are accepted for incoming traffic
@@ -12,19 +16,14 @@ port = 4242
"bc:05:ab:7a:a4:de:88:8c:2f:92:ac:bc:b8:49:b8:24:0d:44:b3:e6:a4:ef:d7:0b:6c:69:6d:77:53:0b:14:80" = "iridium"
# define a client on the right side with host name "iridium"
[[clients]]
# position (left | right | top | bottom)
position = "right"
[right]
# hostname
hostname = "iridium"
# activate this client immediately when lan-mouse is started
activate_on_startup = true
# optional list of (known) ip addresses
ips = ["192.168.178.156"]
# define a client on the left side with IP address 192.168.178.189
[[clients]]
position = "left"
[left]
# The hostname is optional: When no hostname is specified,
# at least one ip address needs to be specified.
hostname = "thorium"

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

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

1
dylibs/.gitignore vendored
View File

@@ -1 +0,0 @@
*

12
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1752687322,
"narHash": "sha256-RKwfXA4OZROjBTQAl9WOZQFm7L8Bo93FQwSJpAiSRvo=",
"lastModified": 1728018373,
"narHash": "sha256-NOiTvBbRLIOe5F6RbHaAh6++BNjsb149fGZd1T4+KBg=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "6e987485eb2c77e5dcc5af4e3c70843711ef9251",
"rev": "bc947f541ae55e999ffdb4013441347d83b00feb",
"type": "github"
},
"original": {
@@ -29,11 +29,11 @@
]
},
"locked": {
"lastModified": 1752806774,
"narHash": "sha256-4cHeoR2roN7d/3J6gT+l6o7J2hTrBIUiCwVdDNMeXzE=",
"lastModified": 1728181869,
"narHash": "sha256-sQXHXsjIcGEoIHkB+RO6BZdrPfB+43V1TEpyoWRI3ww=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "3c90219b3ba1c9790c45a078eae121de48a39c55",
"rev": "cd46aa3906c14790ef5cbe278d9e54f2c38f95c0",
"type": "github"
},
"original": {

View File

@@ -40,21 +40,21 @@ wayland-protocols-wlr = { version = "0.3.1", features = [
"client",
], optional = true }
x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
ashpd = { version = "0.11.0", default-features = false, features = [
ashpd = { version = "0.10", default-features = false, features = [
"tokio",
], optional = true }
reis = { version = "0.5.0", features = ["tokio"], optional = true }
reis = { version = "0.4", features = ["tokio"], optional = true }
[target.'cfg(target_os="macos")'.dependencies]
core-graphics = { version = "0.25.0", features = ["highsierra"] }
core-graphics = { version = "0.24.0", features = ["highsierra"] }
core-foundation = "0.10.0"
core-foundation-sys = "0.8.6"
libc = "0.2.155"
keycode = "1.0.0"
keycode = "0.4.0"
bitflags = "2.6.0"
[target.'cfg(windows)'.dependencies]
windows = { version = "0.61.2", features = [
windows = { version = "0.58.0", features = [
"Win32_System_LibraryLoader",
"Win32_System_Threading",
"Win32_Foundation",

View File

@@ -1,9 +1,8 @@
use async_trait::async_trait;
use futures_core::Stream;
use std::{
collections::{HashSet, VecDeque},
collections::VecDeque,
env,
fmt::{self, Display},
io::{self, ErrorKind},
os::fd::{AsFd, RawFd},
pin::Pin,
@@ -47,15 +46,13 @@ use wayland_protocols_wlr::layer_shell::v1::client::{
use wayland_client::{
backend::{ReadEventsGuard, WaylandError},
delegate_noop,
globals::{registry_queue_init, Global, GlobalList, GlobalListContents},
globals::{registry_queue_init, GlobalListContents},
protocol::{
wl_buffer, wl_compositor,
wl_keyboard::{self, WlKeyboard},
wl_output::{self, WlOutput},
wl_pointer::{self, WlPointer},
wl_region,
wl_registry::{self, WlRegistry},
wl_seat, wl_shm, wl_shm_pool,
wl_region, wl_registry, wl_seat, wl_shm, wl_shm_pool,
wl_surface::WlSurface,
},
Connection, Dispatch, DispatchError, EventQueue, QueueHandle, WEnum,
@@ -78,42 +75,28 @@ struct Globals {
seat: wl_seat::WlSeat,
shm: wl_shm::WlShm,
layer_shell: ZwlrLayerShellV1,
outputs: Vec<WlOutput>,
xdg_output_manager: ZxdgOutputManagerV1,
}
#[derive(Clone, Debug)]
struct Output {
wl_output: WlOutput,
global: Global,
info: Option<OutputInfo>,
pending_info: OutputInfo,
has_xdg_info: bool,
}
impl Display for Output {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(info) = &self.info {
write!(
f,
"{} {}x{} @pos {:?} ({})",
info.name, info.size.0, info.size.1, info.position, info.description
)
} else {
write!(f, "unknown output")
}
}
}
#[derive(Clone, Debug, Default)]
#[derive(Debug, Clone)]
struct OutputInfo {
description: String,
name: String,
position: (i32, i32),
size: (i32, i32),
}
impl OutputInfo {
fn new() -> Self {
Self {
name: "".to_string(),
position: (0, 0),
size: (0, 0),
}
}
}
struct State {
active_positions: HashSet<Position>,
pointer: Option<WlPointer>,
keyboard: Option<WlKeyboard>,
pointer_lock: Option<ZwpLockedPointerV1>,
@@ -121,13 +104,12 @@ struct State {
shortcut_inhibitor: Option<ZwpKeyboardShortcutsInhibitorV1>,
active_windows: Vec<Arc<Window>>,
focused: Option<Arc<Window>>,
global_list: GlobalList,
globals: Globals,
g: Globals,
wayland_fd: RawFd,
read_guard: Option<ReadEventsGuard>,
qh: QueueHandle<Self>,
pending_events: VecDeque<(Position, CaptureEvent)>,
outputs: Vec<Output>,
output_info: Vec<(WlOutput, OutputInfo)>,
scroll_discrete_pending: bool,
}
@@ -160,7 +142,7 @@ impl Window {
size: (i32, i32),
) -> Window {
log::debug!("creating window output: {output:?}, size: {size:?}");
let g = &state.globals;
let g = &state.g;
let (width, height) = match pos {
Position::Left | Position::Right => (1, size.1 as u32),
@@ -221,36 +203,41 @@ impl Drop for Window {
}
}
fn get_edges(outputs: &[Output], pos: Position) -> Vec<(Output, i32)> {
fn get_edges(outputs: &[(WlOutput, OutputInfo)], pos: Position) -> Vec<(WlOutput, i32)> {
outputs
.iter()
.filter_map(|output| {
output.info.as_ref().map(|info| {
(
output.clone(),
match pos {
Position::Left => info.position.0,
Position::Right => info.position.0 + info.size.0,
Position::Top => info.position.1,
Position::Bottom => info.position.1 + info.size.1,
},
)
})
.map(|(o, i)| {
(
o.clone(),
match pos {
Position::Left => i.position.0,
Position::Right => i.position.0 + i.size.0,
Position::Top => i.position.1,
Position::Bottom => i.position.1 + i.size.1,
},
)
})
.collect()
}
fn get_output_configuration(state: &State, pos: Position) -> Vec<Output> {
fn get_output_configuration(state: &State, pos: Position) -> Vec<(WlOutput, OutputInfo)> {
// get all output edges corresponding to the position
let edges = get_edges(&state.outputs, pos);
let opposite_edges = get_edges(&state.outputs, pos.opposite());
let edges = get_edges(&state.output_info, pos);
log::debug!("edges: {edges:?}");
let opposite_edges = get_edges(&state.output_info, pos.opposite());
// remove those edges that are at the same position
// as an opposite edge of a different output
edges
let outputs: Vec<WlOutput> = edges
.iter()
.filter(|(_, edge)| !opposite_edges.iter().map(|(_, e)| *e).any(|e| &e == edge))
.map(|(o, _)| o.clone())
.collect();
state
.output_info
.iter()
.filter(|(o, _)| outputs.contains(o))
.map(|(o, i)| (o.clone(), i.clone()))
.collect()
}
@@ -272,36 +259,36 @@ fn draw(f: &mut File, (width, height): (u32, u32)) {
impl LayerShellInputCapture {
pub fn new() -> std::result::Result<Self, LayerShellCaptureCreationError> {
let conn = Connection::connect_to_env()?;
let (global_list, mut queue) = registry_queue_init::<State>(&conn)?;
let (g, mut queue) = registry_queue_init::<State>(&conn)?;
let qh = queue.handle();
let compositor: wl_compositor::WlCompositor = global_list
let compositor: wl_compositor::WlCompositor = g
.bind(&qh, 4..=5, ())
.map_err(|e| WaylandBindError::new(e, "wl_compositor 4..=5"))?;
let xdg_output_manager: ZxdgOutputManagerV1 = global_list
let xdg_output_manager: ZxdgOutputManagerV1 = g
.bind(&qh, 1..=3, ())
.map_err(|e| WaylandBindError::new(e, "xdg_output_manager 1..=3"))?;
let shm: wl_shm::WlShm = global_list
let shm: wl_shm::WlShm = g
.bind(&qh, 1..=1, ())
.map_err(|e| WaylandBindError::new(e, "wl_shm"))?;
let layer_shell: ZwlrLayerShellV1 = global_list
let layer_shell: ZwlrLayerShellV1 = g
.bind(&qh, 3..=4, ())
.map_err(|e| WaylandBindError::new(e, "wlr_layer_shell 3..=4"))?;
let seat: wl_seat::WlSeat = global_list
let seat: wl_seat::WlSeat = g
.bind(&qh, 7..=8, ())
.map_err(|e| WaylandBindError::new(e, "wl_seat 7..=8"))?;
let pointer_constraints: ZwpPointerConstraintsV1 = global_list
let pointer_constraints: ZwpPointerConstraintsV1 = g
.bind(&qh, 1..=1, ())
.map_err(|e| WaylandBindError::new(e, "zwp_pointer_constraints_v1"))?;
let relative_pointer_manager: ZwpRelativePointerManagerV1 = global_list
let relative_pointer_manager: ZwpRelativePointerManagerV1 = g
.bind(&qh, 1..=1, ())
.map_err(|e| WaylandBindError::new(e, "zwp_relative_pointer_manager_v1"))?;
let shortcut_inhibit_manager: Result<
ZwpKeyboardShortcutsInhibitManagerV1,
WaylandBindError,
> = global_list
> = g
.bind(&qh, 1..=1, ())
.map_err(|e| WaylandBindError::new(e, "zwp_keyboard_shortcuts_inhibit_manager_v1"));
// layer-shell backend still works without this protocol so we make it an optional dependency
@@ -310,41 +297,65 @@ impl LayerShellInputCapture {
to the client");
}
let shortcut_inhibit_manager = shortcut_inhibit_manager.ok();
let outputs = vec![];
let g = Globals {
compositor,
shm,
layer_shell,
seat,
pointer_constraints,
relative_pointer_manager,
shortcut_inhibit_manager,
outputs,
xdg_output_manager,
};
// flush outgoing events
queue.flush()?;
let wayland_fd = queue.as_fd().as_raw_fd();
let mut state = State {
active_positions: Default::default(),
pointer: None,
keyboard: None,
global_list,
globals: Globals {
compositor,
shm,
layer_shell,
seat,
pointer_constraints,
relative_pointer_manager,
shortcut_inhibit_manager,
xdg_output_manager,
},
g,
pointer_lock: None,
rel_pointer: None,
shortcut_inhibitor: None,
active_windows: Vec::new(),
focused: None,
qh,
wayland_fd: queue.as_fd().as_raw_fd(),
wayland_fd,
read_guard: None,
pending_events: VecDeque::new(),
outputs: vec![],
output_info: vec![],
scroll_discrete_pending: false,
};
for global in state.global_list.contents().clone_list() {
state.register_global(global);
// dispatch registry to () again, in order to read all wl_outputs
conn.display().get_registry(&state.qh, ());
log::debug!("==============> requested registry");
// roundtrip to read wl_output globals
queue.roundtrip(&mut state)?;
log::debug!("==============> roundtrip 1 done");
// read outputs
for output in state.g.outputs.iter() {
state
.g
.xdg_output_manager
.get_xdg_output(output, &state.qh, output.clone());
}
// flush outgoing events
queue.flush()?;
// roundtrip to read xdg_output events
queue.roundtrip(&mut state)?;
log::debug!("==============> roundtrip 2 done");
for i in &state.output_info {
log::debug!("{:#?}", i.1);
}
let read_guard = loop {
match queue.prepare_read() {
@@ -368,7 +379,6 @@ impl LayerShellInputCapture {
fn delete_client(&mut self, pos: Position) {
let inner = self.0.get_mut();
inner.state.active_positions.remove(&pos);
// remove all windows corresponding to this client
while let Some(i) = inner.state.active_windows.iter().position(|w| w.pos == pos) {
inner.state.active_windows.remove(i);
@@ -378,52 +388,6 @@ impl LayerShellInputCapture {
}
impl State {
fn update_output_info(&mut self, name: u32) {
let output = self
.outputs
.iter_mut()
.find(|o| o.global.name == name)
.expect("output not found");
if output.has_xdg_info {
output.info.replace(output.pending_info.clone());
self.update_windows();
}
}
fn register_global(&mut self, global: Global) {
if global.interface.as_str() == "wl_output" {
log::debug!("new output global: wl_output {}", global.name);
let wl_output = self.global_list.registry().bind::<WlOutput, _, _>(
global.name,
4,
&self.qh,
global.name,
);
self.globals
.xdg_output_manager
.get_xdg_output(&wl_output, &self.qh, global.name);
self.outputs.push(Output {
wl_output,
global,
info: None,
has_xdg_info: false,
pending_info: Default::default(),
})
}
}
fn deregister_global(&mut self, name: u32) {
self.outputs.retain(|o| {
if o.global.name == name {
log::debug!("{o} (global {:?}) removed", o.global);
o.wl_output.release();
false
} else {
true
}
});
}
fn grab(
&mut self,
surface: &WlSurface,
@@ -444,7 +408,7 @@ impl State {
// lock pointer
if self.pointer_lock.is_none() {
self.pointer_lock = Some(self.globals.pointer_constraints.lock_pointer(
self.pointer_lock = Some(self.g.pointer_constraints.lock_pointer(
surface,
pointer,
None,
@@ -456,7 +420,7 @@ impl State {
// request relative input
if self.rel_pointer.is_none() {
self.rel_pointer = Some(self.globals.relative_pointer_manager.get_relative_pointer(
self.rel_pointer = Some(self.g.relative_pointer_manager.get_relative_pointer(
pointer,
qh,
(),
@@ -464,14 +428,10 @@ impl State {
}
// capture modifier keys
if let Some(shortcut_inhibit_manager) = &self.globals.shortcut_inhibit_manager {
if let Some(shortcut_inhibit_manager) = &self.g.shortcut_inhibit_manager {
if self.shortcut_inhibitor.is_none() {
self.shortcut_inhibitor = Some(shortcut_inhibit_manager.inhibit_shortcuts(
surface,
&self.globals.seat,
qh,
(),
));
self.shortcut_inhibitor =
Some(shortcut_inhibit_manager.inhibit_shortcuts(surface, &self.g.seat, qh, ()));
}
}
}
@@ -509,39 +469,21 @@ impl State {
}
fn add_client(&mut self, pos: Position) {
self.active_positions.insert(pos);
let outputs = get_output_configuration(self, pos);
log::info!(
"adding capture for position {pos} - using outputs: {:?}",
outputs
.iter()
.map(|o| o
.info
.as_ref()
.map(|i| i.name.to_owned())
.unwrap_or("unknown output".to_owned()))
.collect::<Vec<_>>()
);
outputs.iter().for_each(|o| {
if let Some(info) = o.info.as_ref() {
let window = Window::new(self, &self.qh, &o.wl_output, pos, info.size);
let window = Arc::new(window);
self.active_windows.push(window);
}
log::debug!("outputs: {outputs:?}");
outputs.iter().for_each(|(o, i)| {
let window = Window::new(self, &self.qh, o, pos, i.size);
let window = Arc::new(window);
self.active_windows.push(window);
});
}
fn update_windows(&mut self) {
log::info!("active outputs: ");
for output in self.outputs.iter().filter(|o| o.info.is_some()) {
log::info!(" * {output}");
}
self.active_windows.clear();
let active_positions = self.active_positions.iter().cloned().collect::<Vec<_>>();
for pos in active_positions {
log::debug!("updating windows");
log::debug!("output info: {:?}", self.output_info);
let clients: Vec<_> = self.active_windows.drain(..).map(|w| w.pos).collect();
for pos in clients {
self.add_client(pos);
}
}
@@ -582,17 +524,17 @@ impl Inner {
match self.queue.dispatch_pending(&mut self.state) {
Ok(_) => {}
Err(DispatchError::Backend(WaylandError::Io(e))) => {
log::error!("Wayland Error: {e}");
log::error!("Wayland Error: {}", e);
}
Err(DispatchError::Backend(e)) => {
panic!("backend error: {e}");
panic!("backend error: {}", e);
}
Err(DispatchError::BadMessage {
sender_id,
interface,
opcode,
}) => {
panic!("bad message {sender_id}, {interface} , {opcode}");
panic!("bad message {}, {} , {}", sender_id, interface, opcode);
}
}
}
@@ -813,7 +755,7 @@ impl Dispatch<WlPointer, ()> for State {
})),
));
}
wl_pointer::Event::Frame => {
wl_pointer::Event::Frame {} => {
// TODO properly handle frame events
// we simply insert a frame event on the client side
// after each event for now
@@ -893,7 +835,7 @@ impl Dispatch<ZwpRelativePointerV1, ()> for State {
} = event
{
if let Some(window) = &app.focused {
let time = ((((utime_hi as u64) << 32) | utime_lo as u64) / 1000) as u32;
let time = (((utime_hi as u64) << 32 | utime_lo as u64) / 1000) as u32;
app.pending_events.push_back((
window.pos,
CaptureEvent::Input(Event::Pointer(PointerEvent::Motion { time, dx, dy })),
@@ -930,89 +872,94 @@ impl Dispatch<ZwlrLayerSurfaceV1, ()> for State {
}
// delegate wl_registry events to App itself
impl Dispatch<WlRegistry, GlobalListContents> for State {
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for State {
fn event(
state: &mut Self,
_registry: &WlRegistry,
event: <WlRegistry as wayland_client::Proxy>::Event,
_state: &mut Self,
_proxy: &wl_registry::WlRegistry,
_event: <wl_registry::WlRegistry as wayland_client::Proxy>::Event,
_data: &GlobalListContents,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_qhandle: &QueueHandle<Self>,
) {
}
}
impl Dispatch<wl_registry::WlRegistry, ()> for State {
fn event(
state: &mut Self,
registry: &wl_registry::WlRegistry,
event: <wl_registry::WlRegistry as wayland_client::Proxy>::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
match event {
wl_registry::Event::Global {
name,
interface,
version,
version: _,
} => {
state.register_global(Global {
name,
interface,
version,
});
}
wl_registry::Event::GlobalRemove { name } => {
state.deregister_global(name);
if interface.as_str() == "wl_output" {
log::debug!("wl_output global");
state
.g
.outputs
.push(registry.bind::<WlOutput, _, _>(name, 4, qh, ()))
}
}
wl_registry::Event::GlobalRemove { .. } => {}
_ => {}
}
}
}
impl Dispatch<ZxdgOutputV1, u32> for State {
impl Dispatch<ZxdgOutputV1, WlOutput> for State {
fn event(
state: &mut Self,
_: &ZxdgOutputV1,
event: <ZxdgOutputV1 as wayland_client::Proxy>::Event,
name: &u32,
wl_output: &WlOutput,
_: &Connection,
_: &QueueHandle<Self>,
) {
let output = state
.outputs
.iter_mut()
.find(|o| o.global.name == *name)
.expect("output");
log::debug!("xdg-output - {event:?}");
let output_info = match state.output_info.iter_mut().find(|(o, _)| o == wl_output) {
Some((_, c)) => c,
None => {
let output_info = OutputInfo::new();
state.output_info.push((wl_output.clone(), output_info));
&mut state.output_info.last_mut().unwrap().1
}
};
log::debug!("xdg_output {name} - {event:?}");
match event {
zxdg_output_v1::Event::LogicalPosition { x, y } => {
output.pending_info.position = (x, y);
output.has_xdg_info = true;
output_info.position = (x, y);
}
zxdg_output_v1::Event::LogicalSize { width, height } => {
output.pending_info.size = (width, height);
output.has_xdg_info = true;
}
zxdg_output_v1::Event::Done => {
log::warn!("Use of deprecated xdg-output event \"done\"");
state.update_output_info(*name);
output_info.size = (width, height);
}
zxdg_output_v1::Event::Done => {}
zxdg_output_v1::Event::Name { name } => {
output.pending_info.name = name;
output.has_xdg_info = true;
output_info.name = name;
}
zxdg_output_v1::Event::Description { description } => {
output.pending_info.description = description;
output.has_xdg_info = true;
}
_ => todo!(),
zxdg_output_v1::Event::Description { .. } => {}
_ => {}
}
}
}
impl Dispatch<WlOutput, u32> for State {
impl Dispatch<WlOutput, ()> for State {
fn event(
state: &mut Self,
_wl_output: &WlOutput,
_proxy: &WlOutput,
event: <WlOutput as wayland_client::Proxy>::Event,
name: &u32,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
log::debug!("wl_output {name} - {event:?}");
if let wl_output::Event::Done = event {
state.update_output_info(*name);
state.update_windows();
}
}
}

View File

@@ -79,7 +79,7 @@ impl Display for Position {
Position::Top => "top",
Position::Bottom => "bottom",
};
write!(f, "{pos}")
write!(f, "{}", pos)
}
}

View File

@@ -153,7 +153,7 @@ async fn update_barriers(
async fn create_session<'a>(
input_capture: &'a InputCapture<'a>,
) -> std::result::Result<(Session<'a, InputCapture<'a>>, BitFlags<Capabilities>), ashpd::Error> {
) -> std::result::Result<(Session<'_, InputCapture<'_>>, BitFlags<Capabilities>), ashpd::Error> {
log::debug!("creating input capture session");
input_capture
.create_session(
@@ -201,7 +201,7 @@ async fn libei_event_handler(
}
}
impl LibeiInputCapture<'_> {
impl<'a> LibeiInputCapture<'a> {
pub async fn new() -> std::result::Result<Self, LibeiCaptureCreationError> {
let input_capture = Box::pin(InputCapture::new().await?);
let input_capture_ptr = input_capture.as_ref().get_ref() as *const InputCapture<'static>;
@@ -561,7 +561,7 @@ async fn handle_ei_event(
}
#[async_trait]
impl LanMouseInputCapture for LibeiInputCapture<'_> {
impl<'a> LanMouseInputCapture for LibeiInputCapture<'a> {
async fn create(&mut self, pos: Position) -> Result<(), CaptureError> {
let _ = self
.notify_capture
@@ -587,18 +587,14 @@ impl LanMouseInputCapture for LibeiInputCapture<'_> {
self.cancellation_token.cancel();
let task = &mut self.capture_task;
log::debug!("waiting for capture to terminate...");
let res = if !task.is_finished() {
task.await.expect("libei task panic")
} else {
Ok(())
};
self.terminated = true;
let res = task.await.expect("libei task panic");
log::debug!("done!");
self.terminated = true;
res
}
}
impl Drop for LibeiInputCapture<'_> {
impl<'a> Drop for LibeiInputCapture<'a> {
fn drop(&mut self) {
if !self.terminated {
/* this workaround is needed until async drop is stabilized */
@@ -607,7 +603,7 @@ impl Drop for LibeiInputCapture<'_> {
}
}
impl Stream for LibeiInputCapture<'_> {
impl<'a> Stream for LibeiInputCapture<'a> {
type Item = Result<(Position, CaptureEvent), CaptureError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {

View File

@@ -10,7 +10,7 @@ use core_graphics::base::{kCGErrorSuccess, CGError};
use core_graphics::display::{CGDisplay, CGPoint};
use core_graphics::event::{
CGEvent, CGEventFlags, CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement,
CGEventTapProxy, CGEventType, CallbackResult, EventField,
CGEventTapProxy, CGEventType, EventField,
};
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
use futures_core::Stream;
@@ -390,15 +390,15 @@ fn create_event_tap<'a>(
if let Some(pos) = pos {
res_events.iter().for_each(|e| {
// error must be ignored, since the event channel
// may already be closed when the InputCapture instance is dropped.
let _ = event_tx.blocking_send((pos, *e));
event_tx
.blocking_send((pos, *e))
.expect("Failed to send event");
});
// Returning Drop should stop the event from being processed
// Returning None should stop the event from being processed
// but core fundation still returns the event
cg_ev.set_type(CGEventType::Null);
}
CallbackResult::Replace(cg_ev.to_owned())
Some(cg_ev.to_owned())
};
let tap = CGEventTap::new(
@@ -411,7 +411,7 @@ fn create_event_tap<'a>(
.map_err(|_| MacosCaptureCreationError::EventTapCreation)?;
let tap_source: CFRunLoopSource = tap
.mach_port()
.mach_port
.create_runloop_source(0)
.expect("Failed creating loop source");
@@ -426,8 +426,8 @@ fn event_tap_thread(
client_state: Arc<Mutex<InputCaptureState>>,
event_tx: Sender<(Position, CaptureEvent)>,
notify_tx: Sender<ProducerEvent>,
ready: std::sync::mpsc::Sender<Result<CFRunLoop, MacosCaptureCreationError>>,
exit: oneshot::Sender<()>,
ready: std::sync::mpsc::Sender<Result<(), MacosCaptureCreationError>>,
exit: oneshot::Sender<Result<(), &'static str>>,
) {
let _tap = match create_event_tap(client_state, notify_tx, event_tx) {
Err(e) => {
@@ -435,22 +435,18 @@ fn event_tap_thread(
return;
}
Ok(tap) => {
let run_loop = CFRunLoop::get_current();
ready.send(Ok(run_loop)).expect("channel closed");
ready.send(Ok(())).expect("channel closed");
tap
}
};
log::debug!("running CFRunLoop...");
CFRunLoop::run_current();
log::debug!("event tap thread exiting!...");
let _ = exit.send(());
let _ = exit.send(Err("tap thread exited"));
}
pub struct MacOSInputCapture {
event_rx: Receiver<(Position, CaptureEvent)>,
notify_tx: Sender<ProducerEvent>,
run_loop: CFRunLoop,
}
impl MacOSInputCapture {
@@ -479,44 +475,36 @@ impl MacOSInputCapture {
});
// wait for event tap creation result
let run_loop = ready_rx.recv().expect("channel closed")?;
ready_rx.recv().expect("channel closed")?;
let _tap_task: tokio::task::JoinHandle<()> = tokio::task::spawn_local(async move {
loop {
tokio::select! {
producer_event = notify_rx.recv() => {
let Some(producer_event) = producer_event else {
break;
};
let producer_event = producer_event.expect("channel closed");
let mut state = state.lock().await;
state.handle_producer_event(producer_event).await.unwrap_or_else(|e| {
log::error!("Failed to handle producer event: {e}");
})
}
_ = &mut tap_exit_rx => {
break;
res = &mut tap_exit_rx => {
if let Err(e) = res.expect("channel closed") {
log::error!("Tap thread failed: {:?}", e);
break;
}
}
}
}
// show cursor
let _ = CGDisplay::show_cursor(&CGDisplay::main());
});
Ok(Self {
event_rx,
notify_tx,
run_loop,
})
}
}
impl Drop for MacOSInputCapture {
fn drop(&mut self) {
self.run_loop.stop();
}
}
#[async_trait]
impl Capture for MacOSInputCapture {
async fn create(&mut self, pos: Position) -> Result<(), CaptureError> {

View File

@@ -1,36 +1,94 @@
use async_trait::async_trait;
use core::task::{Context, Poll};
use event_thread::EventThread;
use futures::Stream;
use std::pin::Pin;
use once_cell::unsync::Lazy;
use std::collections::HashSet;
use std::ptr::{addr_of, addr_of_mut};
use futures::executor::block_on;
use std::default::Default;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::{mpsc, Mutex};
use std::task::ready;
use tokio::sync::mpsc::{channel, Receiver};
use std::{pin::Pin, thread};
use tokio::sync::mpsc::{channel, Receiver, Sender};
use windows::core::{w, PCWSTR};
use windows::Win32::Foundation::{FALSE, HINSTANCE, HWND, LPARAM, LRESULT, RECT, WPARAM};
use windows::Win32::Graphics::Gdi::{
EnumDisplayDevicesW, EnumDisplaySettingsW, DEVMODEW, DISPLAY_DEVICEW,
DISPLAY_DEVICE_ATTACHED_TO_DESKTOP, ENUM_CURRENT_SETTINGS,
};
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::System::Threading::GetCurrentThreadId;
use windows::Win32::UI::WindowsAndMessaging::{
CallNextHookEx, CreateWindowExW, DispatchMessageW, GetMessageW, PostThreadMessageW,
RegisterClassW, SetWindowsHookExW, TranslateMessage, EDD_GET_DEVICE_INTERFACE_NAME, HHOOK,
HMENU, HOOKPROC, KBDLLHOOKSTRUCT, LLKHF_EXTENDED, MSG, MSLLHOOKSTRUCT, WH_KEYBOARD_LL,
WH_MOUSE_LL, WINDOW_STYLE, WM_DISPLAYCHANGE, WM_KEYDOWN, WM_KEYUP, WM_LBUTTONDOWN,
WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_RBUTTONDOWN,
WM_RBUTTONUP, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_USER, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSW,
WNDPROC,
};
use input_event::{
scancode::{self, Linux},
Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT,
};
use super::{Capture, CaptureError, CaptureEvent, Position};
mod display_util;
mod event_thread;
enum Request {
Create(Position),
Destroy(Position),
}
pub struct WindowsInputCapture {
event_rx: Receiver<(Position, CaptureEvent)>,
event_thread: EventThread,
msg_thread: Option<std::thread::JoinHandle<()>>,
}
enum EventType {
Request = 0,
Release = 1,
Exit = 2,
}
unsafe fn signal_message_thread(event_type: EventType) {
if let Some(event_tid) = get_event_tid() {
PostThreadMessageW(event_tid, WM_USER, WPARAM(event_type as usize), LPARAM(0)).unwrap();
} else {
panic!();
}
}
#[async_trait]
impl Capture for WindowsInputCapture {
async fn create(&mut self, pos: Position) -> Result<(), CaptureError> {
self.event_thread.create(pos);
unsafe {
{
let mut requests = REQUEST_BUFFER.lock().unwrap();
requests.push(Request::Create(pos));
}
signal_message_thread(EventType::Request);
}
Ok(())
}
async fn destroy(&mut self, pos: Position) -> Result<(), CaptureError> {
self.event_thread.destroy(pos);
unsafe {
{
let mut requests = REQUEST_BUFFER.lock().unwrap();
requests.push(Request::Destroy(pos));
}
signal_message_thread(EventType::Request);
}
Ok(())
}
async fn release(&mut self) -> Result<(), CaptureError> {
self.event_thread.release_capture();
unsafe { signal_message_thread(EventType::Release) };
Ok(())
}
@@ -39,13 +97,519 @@ impl Capture for WindowsInputCapture {
}
}
static mut REQUEST_BUFFER: Mutex<Vec<Request>> = Mutex::new(Vec::new());
static mut ACTIVE_CLIENT: Option<Position> = None;
static mut CLIENTS: Lazy<HashSet<Position>> = Lazy::new(HashSet::new);
static mut EVENT_TX: Option<Sender<(Position, CaptureEvent)>> = None;
static mut EVENT_THREAD_ID: AtomicU32 = AtomicU32::new(0);
unsafe fn set_event_tid(tid: u32) {
EVENT_THREAD_ID.store(tid, Ordering::SeqCst);
}
unsafe fn get_event_tid() -> Option<u32> {
match EVENT_THREAD_ID.load(Ordering::SeqCst) {
0 => None,
id => Some(id),
}
}
static mut ENTRY_POINT: (i32, i32) = (0, 0);
fn to_mouse_event(wparam: WPARAM, lparam: LPARAM) -> Option<PointerEvent> {
let mouse_low_level: MSLLHOOKSTRUCT = unsafe { *(lparam.0 as *const MSLLHOOKSTRUCT) };
match wparam {
WPARAM(p) if p == WM_LBUTTONDOWN as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_LEFT,
state: 1,
}),
WPARAM(p) if p == WM_MBUTTONDOWN as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_MIDDLE,
state: 1,
}),
WPARAM(p) if p == WM_RBUTTONDOWN as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_RIGHT,
state: 1,
}),
WPARAM(p) if p == WM_LBUTTONUP as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_LEFT,
state: 0,
}),
WPARAM(p) if p == WM_MBUTTONUP as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_MIDDLE,
state: 0,
}),
WPARAM(p) if p == WM_RBUTTONUP as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_RIGHT,
state: 0,
}),
WPARAM(p) if p == WM_MOUSEMOVE as usize => unsafe {
let (x, y) = (mouse_low_level.pt.x, mouse_low_level.pt.y);
let (ex, ey) = ENTRY_POINT;
let (dx, dy) = (x - ex, y - ey);
let (dx, dy) = (dx as f64, dy as f64);
Some(PointerEvent::Motion { time: 0, dx, dy })
},
WPARAM(p) if p == WM_MOUSEWHEEL as usize => Some(PointerEvent::AxisDiscrete120 {
axis: 0,
value: -(mouse_low_level.mouseData as i32 >> 16),
}),
WPARAM(p) if p == WM_XBUTTONDOWN as usize || p == WM_XBUTTONUP as usize => {
let hb = mouse_low_level.mouseData >> 16;
let button = match hb {
1 => BTN_BACK,
2 => BTN_FORWARD,
_ => {
log::warn!("unknown mouse button");
return None;
}
};
Some(PointerEvent::Button {
time: 0,
button,
state: if p == WM_XBUTTONDOWN as usize { 1 } else { 0 },
})
}
w => {
log::warn!("unknown mouse event: {w:?}");
None
}
}
}
unsafe fn to_key_event(wparam: WPARAM, lparam: LPARAM) -> Option<KeyboardEvent> {
let kybrdllhookstruct: KBDLLHOOKSTRUCT = *(lparam.0 as *const KBDLLHOOKSTRUCT);
let mut scan_code = kybrdllhookstruct.scanCode;
log::trace!("scan_code: {scan_code}");
if kybrdllhookstruct.flags.contains(LLKHF_EXTENDED) {
scan_code |= 0xE000;
}
let Ok(win_scan_code) = scancode::Windows::try_from(scan_code) else {
log::warn!("failed to translate to windows scancode: {scan_code}");
return None;
};
log::trace!("windows_scan: {win_scan_code:?}");
let Ok(linux_scan_code): Result<Linux, ()> = win_scan_code.try_into() else {
log::warn!("failed to translate into linux scancode: {win_scan_code:?}");
return None;
};
log::trace!("windows_scan: {linux_scan_code:?}");
let scan_code = linux_scan_code as u32;
match wparam {
WPARAM(p) if p == WM_KEYDOWN as usize => Some(KeyboardEvent::Key {
time: 0,
key: scan_code,
state: 1,
}),
WPARAM(p) if p == WM_KEYUP as usize => Some(KeyboardEvent::Key {
time: 0,
key: scan_code,
state: 0,
}),
WPARAM(p) if p == WM_SYSKEYDOWN as usize => Some(KeyboardEvent::Key {
time: 0,
key: scan_code,
state: 1,
}),
WPARAM(p) if p == WM_SYSKEYUP as usize => Some(KeyboardEvent::Key {
time: 0,
key: scan_code,
state: 1,
}),
_ => None,
}
}
///
/// clamp point to display bounds
///
/// # Arguments
///
/// * `prev_point`: coordinates, the cursor was before entering, within bounds of a display
/// * `entry_point`: point to clamp
///
/// returns: (i32, i32), the corrected entry point
///
fn clamp_to_display_bounds(prev_point: (i32, i32), point: (i32, i32)) -> (i32, i32) {
/* find display where movement came from */
let display_regions = unsafe { get_display_regions() };
let display = display_regions
.iter()
.find(|&d| is_within_dp_region(prev_point, d))
.unwrap();
/* clamp to bounds (inclusive) */
let (x, y) = point;
let (min_x, max_x) = (display.left, display.right - 1);
let (min_y, max_y) = (display.top, display.bottom - 1);
(x.clamp(min_x, max_x), y.clamp(min_y, max_y))
}
unsafe fn send_blocking(event: CaptureEvent) {
if let Some(active) = ACTIVE_CLIENT {
block_on(async move {
let _ = EVENT_TX.as_ref().unwrap().send((active, event)).await;
});
}
}
unsafe fn check_client_activation(wparam: WPARAM, lparam: LPARAM) -> bool {
if wparam.0 != WM_MOUSEMOVE as usize {
return ACTIVE_CLIENT.is_some();
}
let mouse_low_level: MSLLHOOKSTRUCT = *(lparam.0 as *const MSLLHOOKSTRUCT);
static mut PREV_POS: Option<(i32, i32)> = None;
let curr_pos = (mouse_low_level.pt.x, mouse_low_level.pt.y);
let prev_pos = PREV_POS.unwrap_or(curr_pos);
PREV_POS.replace(curr_pos);
/* next event is the first actual event */
let ret = ACTIVE_CLIENT.is_some();
/* client already active, no need to check */
if ACTIVE_CLIENT.is_some() {
return ret;
}
/* check if a client was activated */
let Some(pos) = entered_barrier(prev_pos, curr_pos, get_display_regions()) else {
return ret;
};
/* check if a client is registered for the barrier */
if !CLIENTS.contains(&pos) {
return ret;
}
/* update active client and entry point */
ACTIVE_CLIENT.replace(pos);
ENTRY_POINT = clamp_to_display_bounds(prev_pos, curr_pos);
/* notify main thread */
log::debug!("ENTERED @ {prev_pos:?} -> {curr_pos:?}");
send_blocking(CaptureEvent::Begin);
ret
}
unsafe extern "system" fn mouse_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
let active = check_client_activation(wparam, lparam);
/* no client was active */
if !active {
return CallNextHookEx(HHOOK::default(), ncode, wparam, lparam);
}
/* get active client if any */
let Some(pos) = ACTIVE_CLIENT else {
return LRESULT(1);
};
/* convert to lan-mouse event */
let Some(pointer_event) = to_mouse_event(wparam, lparam) else {
return LRESULT(1);
};
let event = (pos, CaptureEvent::Input(Event::Pointer(pointer_event)));
/* notify mainthread (drop events if sending too fast) */
if let Err(e) = EVENT_TX.as_ref().unwrap().try_send(event) {
log::warn!("e: {e}");
}
/* don't pass event to applications */
LRESULT(1)
}
unsafe extern "system" fn kybrd_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
/* get active client if any */
let Some(client) = ACTIVE_CLIENT else {
return CallNextHookEx(HHOOK::default(), ncode, wparam, lparam);
};
/* convert to key event */
let Some(key_event) = to_key_event(wparam, lparam) else {
return LRESULT(1);
};
let event = (client, CaptureEvent::Input(Event::Keyboard(key_event)));
if let Err(e) = EVENT_TX.as_ref().unwrap().try_send(event) {
log::warn!("e: {e}");
}
/* don't pass event to applications */
LRESULT(1)
}
unsafe extern "system" fn window_proc(
_hwnd: HWND,
uint: u32,
_wparam: WPARAM,
_lparam: LPARAM,
) -> LRESULT {
match uint {
x if x == WM_DISPLAYCHANGE => {
log::debug!("display resolution changed");
DISPLAY_RESOLUTION_CHANGED = true;
}
_ => {}
}
LRESULT(1)
}
fn enumerate_displays() -> Vec<RECT> {
unsafe {
let mut display_rects = vec![];
let mut devices = vec![];
for i in 0.. {
let mut device: DISPLAY_DEVICEW = std::mem::zeroed();
device.cb = std::mem::size_of::<DISPLAY_DEVICEW>() as u32;
let ret = EnumDisplayDevicesW(None, i, &mut device, EDD_GET_DEVICE_INTERFACE_NAME);
if ret == FALSE {
break;
}
if device.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP != 0 {
devices.push(device.DeviceName);
}
}
for device in devices {
let mut dev_mode: DEVMODEW = std::mem::zeroed();
dev_mode.dmSize = std::mem::size_of::<DEVMODEW>() as u16;
let ret = EnumDisplaySettingsW(
PCWSTR::from_raw(&device as *const _),
ENUM_CURRENT_SETTINGS,
&mut dev_mode,
);
if ret == FALSE {
log::warn!("no display mode");
}
let pos = dev_mode.Anonymous1.Anonymous2.dmPosition;
let (x, y) = (pos.x, pos.y);
let (width, height) = (dev_mode.dmPelsWidth, dev_mode.dmPelsHeight);
display_rects.push(RECT {
left: x,
right: x + width as i32,
top: y,
bottom: y + height as i32,
});
}
display_rects
}
}
static mut DISPLAY_RESOLUTION_CHANGED: bool = true;
unsafe fn get_display_regions() -> &'static Vec<RECT> {
static mut DISPLAYS: Vec<RECT> = vec![];
if DISPLAY_RESOLUTION_CHANGED {
DISPLAYS = enumerate_displays();
DISPLAY_RESOLUTION_CHANGED = false;
log::debug!("displays: {DISPLAYS:?}");
}
&*addr_of!(DISPLAYS)
}
fn is_within_dp_region(point: (i32, i32), display: &RECT) -> bool {
[
Position::Left,
Position::Right,
Position::Top,
Position::Bottom,
]
.iter()
.all(|&pos| is_within_dp_boundary(point, display, pos))
}
fn is_within_dp_boundary(point: (i32, i32), display: &RECT, pos: Position) -> bool {
let (x, y) = point;
match pos {
Position::Left => display.left <= x,
Position::Right => display.right > x,
Position::Top => display.top <= y,
Position::Bottom => display.bottom > y,
}
}
/// returns whether the given position is within the display bounds with respect to the given
/// barrier position
///
/// # Arguments
///
/// * `x`:
/// * `y`:
/// * `displays`:
/// * `pos`:
///
/// returns: bool
///
fn in_bounds(point: (i32, i32), displays: &[RECT], pos: Position) -> bool {
displays
.iter()
.any(|d| is_within_dp_boundary(point, d, pos))
}
fn in_display_region(point: (i32, i32), displays: &[RECT]) -> bool {
displays.iter().any(|d| is_within_dp_region(point, d))
}
fn moved_across_boundary(
prev_pos: (i32, i32),
curr_pos: (i32, i32),
displays: &[RECT],
pos: Position,
) -> bool {
/* was within bounds, but is not anymore */
in_display_region(prev_pos, displays) && !in_bounds(curr_pos, displays, pos)
}
fn entered_barrier(
prev_pos: (i32, i32),
curr_pos: (i32, i32),
displays: &[RECT],
) -> Option<Position> {
[
Position::Left,
Position::Right,
Position::Top,
Position::Bottom,
]
.into_iter()
.find(|&pos| moved_across_boundary(prev_pos, curr_pos, displays, pos))
}
fn get_msg() -> Option<MSG> {
unsafe {
let mut msg = std::mem::zeroed();
let ret = GetMessageW(addr_of_mut!(msg), HWND::default(), 0, 0);
match ret.0 {
0 => None,
x if x > 0 => Some(msg),
_ => panic!("error in GetMessageW"),
}
}
}
static WINDOW_CLASS_REGISTERED: AtomicBool = AtomicBool::new(false);
fn message_thread(ready_tx: mpsc::Sender<()>) {
unsafe {
set_event_tid(GetCurrentThreadId());
ready_tx.send(()).expect("channel closed");
let mouse_proc: HOOKPROC = Some(mouse_proc);
let kybrd_proc: HOOKPROC = Some(kybrd_proc);
let window_proc: WNDPROC = Some(window_proc);
/* register hooks */
let _ = SetWindowsHookExW(WH_MOUSE_LL, mouse_proc, HINSTANCE::default(), 0).unwrap();
let _ = SetWindowsHookExW(WH_KEYBOARD_LL, kybrd_proc, HINSTANCE::default(), 0).unwrap();
let instance = GetModuleHandleW(None).unwrap();
let window_class: WNDCLASSW = WNDCLASSW {
lpfnWndProc: window_proc,
hInstance: instance.into(),
lpszClassName: w!("lan-mouse-message-window-class"),
..Default::default()
};
if WINDOW_CLASS_REGISTERED
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
/* register window class if not yet done so */
let ret = RegisterClassW(&window_class);
if ret == 0 {
panic!("RegisterClassW");
}
}
/* window is used ro receive WM_DISPLAYCHANGE messages */
CreateWindowExW(
Default::default(),
w!("lan-mouse-message-window-class"),
w!("lan-mouse-msg-window"),
WINDOW_STYLE::default(),
0,
0,
0,
0,
HWND::default(),
HMENU::default(),
instance,
None,
)
.expect("CreateWindowExW");
/* run message loop */
loop {
// mouse / keybrd proc do not actually return a message
let Some(msg) = get_msg() else {
break;
};
if msg.hwnd.0.is_null() {
/* messages sent via PostThreadMessage */
match msg.wParam.0 {
x if x == EventType::Exit as usize => break,
x if x == EventType::Release as usize => {
ACTIVE_CLIENT.take();
}
x if x == EventType::Request as usize => {
let requests = {
let mut res = vec![];
let mut requests = REQUEST_BUFFER.lock().unwrap();
for request in requests.drain(..) {
res.push(request);
}
res
};
for request in requests {
update_clients(request)
}
}
_ => {}
}
} else {
/* other messages for window_procs */
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
}
}
fn update_clients(request: Request) {
match request {
Request::Create(pos) => {
unsafe { CLIENTS.insert(pos) };
}
Request::Destroy(pos) => unsafe {
if let Some(active_pos) = ACTIVE_CLIENT {
if pos == active_pos {
let _ = ACTIVE_CLIENT.take();
}
}
CLIENTS.remove(&pos);
},
}
}
impl WindowsInputCapture {
pub(crate) fn new() -> Self {
let (event_tx, event_rx) = channel(10);
let event_thread = EventThread::new(event_tx);
Self {
event_thread,
event_rx,
unsafe {
let (tx, rx) = channel(10);
EVENT_TX.replace(tx);
let (ready_tx, ready_rx) = mpsc::channel();
let msg_thread = Some(thread::spawn(|| message_thread(ready_tx)));
/* wait for thread to set its id */
ready_rx.recv().expect("channel closed");
Self {
msg_thread,
event_rx: rx,
}
}
}
}
@@ -59,3 +623,10 @@ impl Stream for WindowsInputCapture {
}
}
}
impl Drop for WindowsInputCapture {
fn drop(&mut self) {
unsafe { signal_message_thread(EventType::Exit) };
let _ = self.msg_thread.take().unwrap().join();
}
}

View File

@@ -1,99 +0,0 @@
use windows::Win32::Foundation::RECT;
use crate::Position;
fn is_within_dp_region(point: (i32, i32), display: &RECT) -> bool {
[
Position::Left,
Position::Right,
Position::Top,
Position::Bottom,
]
.iter()
.all(|&pos| is_within_dp_boundary(point, display, pos))
}
fn is_within_dp_boundary(point: (i32, i32), display: &RECT, pos: Position) -> bool {
let (x, y) = point;
match pos {
Position::Left => display.left <= x,
Position::Right => display.right > x,
Position::Top => display.top <= y,
Position::Bottom => display.bottom > y,
}
}
/// returns whether the given position is within the display bounds with respect to the given
/// barrier position
///
/// # Arguments
///
/// * `x`:
/// * `y`:
/// * `displays`:
/// * `pos`:
///
/// returns: bool
///
fn in_bounds(point: (i32, i32), displays: &[RECT], pos: Position) -> bool {
displays
.iter()
.any(|d| is_within_dp_boundary(point, d, pos))
}
fn in_display_region(point: (i32, i32), displays: &[RECT]) -> bool {
displays.iter().any(|d| is_within_dp_region(point, d))
}
fn moved_across_boundary(
prev_pos: (i32, i32),
curr_pos: (i32, i32),
displays: &[RECT],
pos: Position,
) -> bool {
/* was within bounds, but is not anymore */
in_display_region(prev_pos, displays) && !in_bounds(curr_pos, displays, pos)
}
pub(crate) fn entered_barrier(
prev_pos: (i32, i32),
curr_pos: (i32, i32),
displays: &[RECT],
) -> Option<Position> {
[
Position::Left,
Position::Right,
Position::Top,
Position::Bottom,
]
.into_iter()
.find(|&pos| moved_across_boundary(prev_pos, curr_pos, displays, pos))
}
///
/// clamp point to display bounds
///
/// # Arguments
///
/// * `prev_point`: coordinates, the cursor was before entering, within bounds of a display
/// * `entry_point`: point to clamp
///
/// returns: (i32, i32), the corrected entry point
///
pub(crate) fn clamp_to_display_bounds(
display_regions: &[RECT],
prev_point: (i32, i32),
point: (i32, i32),
) -> (i32, i32) {
/* find display where movement came from */
let display = display_regions
.iter()
.find(|&d| is_within_dp_region(prev_point, d))
.unwrap();
/* clamp to bounds (inclusive) */
let (x, y) = point;
let (min_x, max_x) = (display.left, display.right - 1);
let (min_y, max_y) = (display.top, display.bottom - 1);
(x.clamp(min_x, max_x), y.clamp(min_y, max_y))
}

View File

@@ -1,553 +0,0 @@
use std::cell::{Cell, RefCell};
use std::collections::HashSet;
use std::ptr::addr_of_mut;
use std::default::Default;
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::thread;
use tokio::sync::mpsc::error::TrySendError;
use tokio::sync::mpsc::Sender;
use windows::core::{w, PCWSTR};
use windows::Win32::Foundation::{FALSE, HWND, LPARAM, LRESULT, RECT, WPARAM};
use windows::Win32::Graphics::Gdi::{
EnumDisplayDevicesW, EnumDisplaySettingsW, DEVMODEW, DISPLAY_DEVICEW,
DISPLAY_DEVICE_ATTACHED_TO_DESKTOP, ENUM_CURRENT_SETTINGS,
};
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::System::Threading::GetCurrentThreadId;
use windows::Win32::UI::WindowsAndMessaging::{
CallNextHookEx, CreateWindowExW, DispatchMessageW, GetMessageW, PostThreadMessageW,
RegisterClassW, SetWindowsHookExW, TranslateMessage, EDD_GET_DEVICE_INTERFACE_NAME, HOOKPROC,
KBDLLHOOKSTRUCT, LLKHF_EXTENDED, MSG, MSLLHOOKSTRUCT, WH_KEYBOARD_LL, WH_MOUSE_LL,
WINDOW_STYLE, WM_DISPLAYCHANGE, WM_KEYDOWN, WM_KEYUP, WM_LBUTTONDOWN, WM_LBUTTONUP,
WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_RBUTTONDOWN,
WM_RBUTTONUP, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_USER, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSW,
WNDPROC,
};
use input_event::{
scancode::{self, Linux},
Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT,
};
use super::{display_util, CaptureEvent, Position};
pub(crate) struct EventThread {
request_buffer: Arc<Mutex<Vec<ClientUpdate>>>,
thread: Option<thread::JoinHandle<()>>,
thread_id: u32,
}
impl EventThread {
pub(crate) fn new(event_tx: Sender<(Position, CaptureEvent)>) -> Self {
let request_buffer = Default::default();
let (thread, thread_id) = start(event_tx, Arc::clone(&request_buffer));
Self {
request_buffer,
thread: Some(thread),
thread_id,
}
}
pub(crate) fn release_capture(&self) {
self.signal(RequestType::Release);
}
pub(crate) fn create(&self, pos: Position) {
self.client_update(ClientUpdate::Create(pos));
}
pub(crate) fn destroy(&self, pos: Position) {
self.client_update(ClientUpdate::Destroy(pos));
}
fn exit(&self) {
self.signal(RequestType::Exit);
}
fn client_update(&self, request: ClientUpdate) {
{
let mut requests = self.request_buffer.lock().unwrap();
requests.push(request);
}
self.signal(RequestType::ClientUpdate);
}
fn signal(&self, event_type: RequestType) {
let id = self.thread_id;
unsafe { PostThreadMessageW(id, WM_USER, WPARAM(event_type as usize), LPARAM(0)).unwrap() };
}
}
impl Drop for EventThread {
fn drop(&mut self) {
self.exit();
let _ = self.thread.take().expect("thread").join();
}
}
enum RequestType {
ClientUpdate = 0,
Release = 1,
Exit = 2,
}
enum ClientUpdate {
Create(Position),
Destroy(Position),
}
fn blocking_send_event(pos: Position, event: CaptureEvent) {
EVENT_TX.with_borrow_mut(|tx| tx.as_mut().unwrap().blocking_send((pos, event)).unwrap())
}
fn try_send_event(
pos: Position,
event: CaptureEvent,
) -> Result<(), TrySendError<(Position, CaptureEvent)>> {
EVENT_TX.with_borrow_mut(|tx| tx.as_mut().unwrap().try_send((pos, event)))
}
thread_local! {
/// all configured clients
static CLIENTS: RefCell<HashSet<Position>> = RefCell::new(HashSet::new());
/// currently active client
static ACTIVE_CLIENT: Cell<Option<Position>> = const { Cell::new(None) };
/// input event channel
static EVENT_TX: RefCell<Option<Sender<(Position, CaptureEvent)>>> = const { RefCell::new(None) };
/// position of barrier entry
static ENTRY_POINT: Cell<(i32, i32)> = const { Cell::new((0, 0)) };
/// previous mouse position
static PREV_POS: Cell<Option<(i32, i32)>> = const { Cell::new(None) };
/// displays and generation counter
static DISPLAYS: RefCell<(Vec<RECT>, i32)> = const { RefCell::new((Vec::new(), 0)) };
}
fn get_msg() -> Option<MSG> {
unsafe {
let mut msg = std::mem::zeroed();
let ret = GetMessageW(addr_of_mut!(msg), None, 0, 0);
match ret.0 {
0 => None,
x if x > 0 => Some(msg),
_ => panic!("error in GetMessageW"),
}
}
}
fn start(
event_tx: Sender<(Position, CaptureEvent)>,
request_buffer: Arc<Mutex<Vec<ClientUpdate>>>,
) -> (thread::JoinHandle<()>, u32) {
/* condition variable to wait for thead id */
let thread_id = Arc::new((Condvar::new(), Mutex::new(None)));
let thread_id_ = Arc::clone(&thread_id);
let msg_thread = thread::spawn(|| start_routine(thread_id_, event_tx, request_buffer));
/* wait for thread to set its id */
let (cond, thread_id) = &*thread_id;
let mut thread_id = thread_id.lock().unwrap();
while (*thread_id).is_none() {
thread_id = cond.wait(thread_id).expect("channel closed");
}
(msg_thread, thread_id.expect("thread id"))
}
fn start_routine(
ready: Arc<(Condvar, Mutex<Option<u32>>)>,
event_tx: Sender<(Position, CaptureEvent)>,
request_buffer: Arc<Mutex<Vec<ClientUpdate>>>,
) {
EVENT_TX.replace(Some(event_tx));
/* communicate thread id */
{
let (cnd, mtx) = &*ready;
let mut ready = mtx.lock().unwrap();
*ready = Some(unsafe { GetCurrentThreadId() });
cnd.notify_one();
}
let mouse_proc: HOOKPROC = Some(mouse_proc);
let kybrd_proc: HOOKPROC = Some(kybrd_proc);
let window_proc: WNDPROC = Some(window_proc);
/* register hooks */
unsafe {
let _ = SetWindowsHookExW(WH_MOUSE_LL, mouse_proc, None, 0).unwrap();
let _ = SetWindowsHookExW(WH_KEYBOARD_LL, kybrd_proc, None, 0).unwrap();
}
let instance = unsafe { GetModuleHandleW(None).unwrap() };
let instance = instance.into();
let window_class: WNDCLASSW = WNDCLASSW {
lpfnWndProc: window_proc,
hInstance: instance,
lpszClassName: w!("lan-mouse-message-window-class"),
..Default::default()
};
static WINDOW_CLASS_REGISTERED: AtomicBool = AtomicBool::new(false);
if WINDOW_CLASS_REGISTERED
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
/* register window class if not yet done so */
unsafe {
let ret = RegisterClassW(&window_class);
if ret == 0 {
panic!("RegisterClassW");
}
}
}
/* window is used ro receive WM_DISPLAYCHANGE messages */
unsafe {
CreateWindowExW(
Default::default(),
w!("lan-mouse-message-window-class"),
w!("lan-mouse-msg-window"),
WINDOW_STYLE::default(),
0,
0,
0,
0,
None,
None,
Some(instance),
None,
)
.expect("CreateWindowExW");
}
/* run message loop */
loop {
// mouse / keybrd proc do not actually return a message
let Some(msg) = get_msg() else {
break;
};
if msg.hwnd.0.is_null() {
/* messages sent via PostThreadMessage */
match msg.wParam.0 {
x if x == RequestType::Exit as usize => break,
x if x == RequestType::Release as usize => {
ACTIVE_CLIENT.take();
}
x if x == RequestType::ClientUpdate as usize => {
let requests = {
let mut res = vec![];
let mut requests = request_buffer.lock().unwrap();
for request in requests.drain(..) {
res.push(request);
}
res
};
for request in requests {
update_clients(request)
}
}
_ => {}
}
} else {
/* other messages for window_procs */
unsafe {
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
}
}
fn check_client_activation(wparam: WPARAM, lparam: LPARAM) -> bool {
if wparam.0 != WM_MOUSEMOVE as usize {
return ACTIVE_CLIENT.get().is_some();
}
let mouse_low_level: MSLLHOOKSTRUCT = unsafe { *(lparam.0 as *const MSLLHOOKSTRUCT) };
let curr_pos = (mouse_low_level.pt.x, mouse_low_level.pt.y);
let prev_pos = PREV_POS.get().unwrap_or(curr_pos);
PREV_POS.replace(Some(curr_pos));
/* next event is the first actual event */
let ret = ACTIVE_CLIENT.get().is_some();
/* client already active, no need to check */
if ACTIVE_CLIENT.get().is_some() {
return ret;
}
/* check if a client was activated */
let entered = DISPLAYS.with_borrow_mut(|(displays, generation)| {
update_display_regions(displays, generation);
display_util::entered_barrier(prev_pos, curr_pos, displays)
});
let Some(pos) = entered else {
return ret;
};
/* check if a client is registered for the barrier */
if !CLIENTS.with_borrow(|clients| clients.contains(&pos)) {
return ret;
}
/* update active client and entry point */
ACTIVE_CLIENT.replace(Some(pos));
let entry_point = DISPLAYS.with_borrow(|(displays, _)| {
display_util::clamp_to_display_bounds(displays, prev_pos, curr_pos)
});
ENTRY_POINT.replace(entry_point);
/* notify main thread */
log::debug!("ENTERED @ {prev_pos:?} -> {curr_pos:?}");
let active = ACTIVE_CLIENT.get().expect("active client");
blocking_send_event(active, CaptureEvent::Begin);
ret
}
unsafe extern "system" fn mouse_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
let active = check_client_activation(wparam, lparam);
/* no client was active */
if !active {
return CallNextHookEx(None, ncode, wparam, lparam);
}
/* get active client if any */
let Some(pos) = ACTIVE_CLIENT.get() else {
return LRESULT(1);
};
/* convert to lan-mouse event */
let Some(pointer_event) = to_mouse_event(wparam, lparam) else {
return LRESULT(1);
};
/* notify mainthread (drop events if sending too fast) */
if let Err(e) = try_send_event(pos, CaptureEvent::Input(Event::Pointer(pointer_event))) {
log::warn!("e: {e}");
}
/* don't pass event to applications */
LRESULT(1)
}
unsafe extern "system" fn kybrd_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
/* get active client if any */
let Some(client) = ACTIVE_CLIENT.get() else {
return CallNextHookEx(None, ncode, wparam, lparam);
};
/* convert to key event */
let Some(key_event) = to_key_event(wparam, lparam) else {
return LRESULT(1);
};
if let Err(e) = try_send_event(client, CaptureEvent::Input(Event::Keyboard(key_event))) {
log::warn!("e: {e}");
}
/* don't pass event to applications */
LRESULT(1)
}
unsafe extern "system" fn window_proc(
_hwnd: HWND,
uint: u32,
_wparam: WPARAM,
_lparam: LPARAM,
) -> LRESULT {
if uint == WM_DISPLAYCHANGE {
log::debug!("display resolution changed");
DISPLAY_RESOLUTION_GENERATION.fetch_add(1, Ordering::Release);
}
LRESULT(1)
}
static DISPLAY_RESOLUTION_GENERATION: AtomicI32 = AtomicI32::new(1);
fn update_display_regions(displays: &mut Vec<RECT>, generation: &mut i32) {
let global_generation = DISPLAY_RESOLUTION_GENERATION.load(Ordering::Acquire);
if *generation != global_generation {
enumerate_displays(displays);
log::debug!("displays: {displays:?}");
*generation = global_generation;
}
}
fn enumerate_displays(display_rects: &mut Vec<RECT>) {
display_rects.clear();
unsafe {
let mut devices = vec![];
for i in 0.. {
let mut device: DISPLAY_DEVICEW = std::mem::zeroed();
device.cb = std::mem::size_of::<DISPLAY_DEVICEW>() as u32;
let ret = EnumDisplayDevicesW(None, i, &mut device, EDD_GET_DEVICE_INTERFACE_NAME);
if ret == FALSE {
break;
}
if device
.StateFlags
.contains(DISPLAY_DEVICE_ATTACHED_TO_DESKTOP)
{
devices.push(device.DeviceName);
}
}
for device in devices {
let mut dev_mode: DEVMODEW = std::mem::zeroed();
dev_mode.dmSize = std::mem::size_of::<DEVMODEW>() as u16;
let ret = EnumDisplaySettingsW(
PCWSTR::from_raw(&device as *const _),
ENUM_CURRENT_SETTINGS,
&mut dev_mode,
);
if ret == FALSE {
log::warn!("no display mode");
}
let pos = dev_mode.Anonymous1.Anonymous2.dmPosition;
let (x, y) = (pos.x, pos.y);
let (width, height) = (dev_mode.dmPelsWidth, dev_mode.dmPelsHeight);
display_rects.push(RECT {
left: x,
right: x + width as i32,
top: y,
bottom: y + height as i32,
});
}
}
}
fn update_clients(request: ClientUpdate) {
match request {
ClientUpdate::Create(pos) => {
CLIENTS.with_borrow_mut(|clients| clients.insert(pos));
}
ClientUpdate::Destroy(pos) => {
if let Some(active_pos) = ACTIVE_CLIENT.get() {
if pos == active_pos {
let _ = ACTIVE_CLIENT.take();
}
}
CLIENTS.with_borrow_mut(|clients| clients.remove(&pos));
}
}
}
fn to_key_event(wparam: WPARAM, lparam: LPARAM) -> Option<KeyboardEvent> {
let kybrdllhookstruct: KBDLLHOOKSTRUCT = unsafe { *(lparam.0 as *const KBDLLHOOKSTRUCT) };
let mut scan_code = kybrdllhookstruct.scanCode;
log::trace!("scan_code: {scan_code}");
if kybrdllhookstruct.flags.contains(LLKHF_EXTENDED) {
scan_code |= 0xE000;
}
let Ok(win_scan_code) = scancode::Windows::try_from(scan_code) else {
log::warn!("failed to translate to windows scancode: {scan_code}");
return None;
};
log::trace!("windows_scan: {win_scan_code:?}");
let Ok(linux_scan_code): Result<Linux, ()> = win_scan_code.try_into() else {
log::warn!("failed to translate into linux scancode: {win_scan_code:?}");
return None;
};
log::trace!("windows_scan: {linux_scan_code:?}");
let scan_code = linux_scan_code as u32;
match wparam {
WPARAM(p) if p == WM_KEYDOWN as usize => Some(KeyboardEvent::Key {
time: 0,
key: scan_code,
state: 1,
}),
WPARAM(p) if p == WM_KEYUP as usize => Some(KeyboardEvent::Key {
time: 0,
key: scan_code,
state: 0,
}),
WPARAM(p) if p == WM_SYSKEYDOWN as usize => Some(KeyboardEvent::Key {
time: 0,
key: scan_code,
state: 1,
}),
WPARAM(p) if p == WM_SYSKEYUP as usize => Some(KeyboardEvent::Key {
time: 0,
key: scan_code,
state: 0,
}),
_ => None,
}
}
fn to_mouse_event(wparam: WPARAM, lparam: LPARAM) -> Option<PointerEvent> {
let mouse_low_level: MSLLHOOKSTRUCT = unsafe { *(lparam.0 as *const MSLLHOOKSTRUCT) };
match wparam {
WPARAM(p) if p == WM_LBUTTONDOWN as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_LEFT,
state: 1,
}),
WPARAM(p) if p == WM_MBUTTONDOWN as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_MIDDLE,
state: 1,
}),
WPARAM(p) if p == WM_RBUTTONDOWN as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_RIGHT,
state: 1,
}),
WPARAM(p) if p == WM_LBUTTONUP as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_LEFT,
state: 0,
}),
WPARAM(p) if p == WM_MBUTTONUP as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_MIDDLE,
state: 0,
}),
WPARAM(p) if p == WM_RBUTTONUP as usize => Some(PointerEvent::Button {
time: 0,
button: BTN_RIGHT,
state: 0,
}),
WPARAM(p) if p == WM_MOUSEMOVE as usize => {
let (x, y) = (mouse_low_level.pt.x, mouse_low_level.pt.y);
let (ex, ey) = ENTRY_POINT.get();
let (dx, dy) = (x - ex, y - ey);
let (dx, dy) = (dx as f64, dy as f64);
Some(PointerEvent::Motion { time: 0, dx, dy })
}
WPARAM(p) if p == WM_MOUSEWHEEL as usize => Some(PointerEvent::AxisDiscrete120 {
axis: 0,
value: -(mouse_low_level.mouseData as i32 >> 16),
}),
WPARAM(p) if p == WM_XBUTTONDOWN as usize || p == WM_XBUTTONUP as usize => {
let hb = mouse_low_level.mouseData >> 16;
let button = match hb {
1 => BTN_BACK,
2 => BTN_FORWARD,
_ => {
log::warn!("unknown mouse button");
return None;
}
};
Some(PointerEvent::Button {
time: 0,
button,
state: if p == WM_XBUTTONDOWN as usize { 1 } else { 0 },
})
}
WPARAM(p) if p == WM_MOUSEHWHEEL as usize => Some(PointerEvent::AxisDiscrete120 {
axis: 1, // Horizontal
value: mouse_low_level.mouseData as i32 >> 16,
}),
w => {
log::warn!("unknown mouse event: {w:?}");
None
}
}
}

View File

@@ -21,12 +21,10 @@ tokio = { version = "1.32.0", features = [
"rt",
"sync",
"signal",
"time"
] }
once_cell = "1.19.0"
[target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
bitflags = "2.6.0"
wayland-client = { version = "0.31.1", optional = true }
wayland-protocols = { version = "0.32.1", features = [
"client",
@@ -40,18 +38,18 @@ wayland-protocols-misc = { version = "0.3.1", features = [
"client",
], optional = true }
x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
ashpd = { version = "0.11.0", default-features = false, features = [
ashpd = { version = "0.10", default-features = false, features = [
"tokio",
], optional = true }
reis = { version = "0.5.0", features = ["tokio"], optional = true }
reis = { version = "0.4", features = ["tokio"], optional = true }
[target.'cfg(target_os="macos")'.dependencies]
bitflags = "2.6.0"
core-graphics = { version = "0.25.0", features = ["highsierra"] }
keycode = "1.0.0"
core-graphics = { version = "0.24.0", features = ["highsierra"] }
keycode = "0.4.0"
[target.'cfg(windows)'.dependencies]
windows = { version = "0.61.2", features = [
windows = { version = "0.58.0", features = [
"Win32_System_LibraryLoader",
"Win32_System_Threading",
"Win32_Foundation",

View File

@@ -74,7 +74,7 @@ async fn get_ei_fd<'a>(
Ok((remote_desktop, session, fd))
}
impl LibeiEmulation<'_> {
impl<'a> LibeiEmulation<'a> {
pub(crate) async fn new() -> Result<Self, LibeiEmulationCreationError> {
let (_remote_desktop, session, eifd) = get_ei_fd().await?;
let stream = UnixStream::from(eifd);
@@ -109,14 +109,14 @@ impl LibeiEmulation<'_> {
}
}
impl Drop for LibeiEmulation<'_> {
impl<'a> Drop for LibeiEmulation<'a> {
fn drop(&mut self) {
self.ei_task.abort();
}
}
#[async_trait]
impl Emulation for LibeiEmulation<'_> {
impl<'a> Emulation for LibeiEmulation<'a> {
async fn consume(
&mut self,
event: Event,

View File

@@ -10,37 +10,25 @@ use core_graphics::event::{
ScrollEventUnit,
};
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
use input_event::{scancode, Event, KeyboardEvent, PointerEvent, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT};
use input_event::{scancode, Event, KeyboardEvent, PointerEvent};
use keycode::{KeyMap, KeyMapping};
use std::cell::Cell;
use std::ops::{Index, IndexMut};
use std::rc::Rc;
use std::sync::Arc;
use std::time::{Duration, Instant};
use std::time::Duration;
use tokio::{sync::Notify, task::JoinHandle};
use super::error::MacOSEmulationCreationError;
const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500);
const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32);
const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(500);
pub(crate) struct MacOSEmulation {
/// global event source for all events
event_source: CGEventSource,
/// task handle for key repeats
repeat_task: Option<JoinHandle<()>>,
/// current state of the mouse buttons
button_state: ButtonState,
/// button previously pressed
previous_button: Option<CGMouseButton>,
/// timestamp of previous click (button down)
previous_button_click: Option<Instant>,
/// click state, i.e. number of clicks in quick succession
button_click_state: i64,
/// current modifier state
modifier_state: Rc<Cell<XMods>>,
/// notify to cancel key repeats
notify_repeat_task: Arc<Notify>,
}
@@ -86,9 +74,6 @@ impl MacOSEmulation {
Ok(Self {
event_source,
button_state,
previous_button: None,
previous_button_click: None,
button_click_state: 1,
repeat_task: None,
notify_repeat_task: Arc::new(Notify::new()),
modifier_state: Rc::new(Cell::new(XMods::empty())),
@@ -104,9 +89,6 @@ impl MacOSEmulation {
// there can only be one repeating key and it's
// always the last to be pressed
self.cancel_repeat_task().await;
// initial key event
key_event(self.event_source.clone(), key, 1, self.modifier_state.get());
// repeat task
let event_source = self.event_source.clone();
let notify = self.notify_repeat_task.clone();
let modifiers = self.modifier_state.clone();
@@ -179,16 +161,16 @@ fn get_display_at_point(x: CGFloat, y: CGFloat) -> Option<CGDirectDisplayID> {
};
if error != 0 {
log::warn!("error getting displays at point ({x}, {y}): {error}");
log::warn!("error getting displays at point ({}, {}): {}", x, y, error);
return Option::None;
}
if display_count == 0 {
log::debug!("no displays found at point ({x}, {y})");
log::debug!("no displays found at point ({}, {})", x, y);
return Option::None;
}
displays.first().copied()
return displays.first().copied();
}
fn get_display_bounds(display: CGDirectDisplayID) -> (CGFloat, CGFloat, CGFloat, CGFloat) {
@@ -242,7 +224,6 @@ impl Emulation for MacOSEmulation {
event: Event,
_handle: EmulationHandle,
) -> Result<(), EmulationError> {
log::trace!("{event:?}");
match event {
Event::Pointer(pointer_event) => match pointer_event {
PointerEvent::Motion { time: _, dx, dy } => {
@@ -290,12 +271,24 @@ impl Emulation for MacOSEmulation {
state,
} => {
let (event_type, mouse_button) = match (button, state) {
(BTN_LEFT, 1) => (CGEventType::LeftMouseDown, CGMouseButton::Left),
(BTN_LEFT, 0) => (CGEventType::LeftMouseUp, CGMouseButton::Left),
(BTN_RIGHT, 1) => (CGEventType::RightMouseDown, CGMouseButton::Right),
(BTN_RIGHT, 0) => (CGEventType::RightMouseUp, CGMouseButton::Right),
(BTN_MIDDLE, 1) => (CGEventType::OtherMouseDown, CGMouseButton::Center),
(BTN_MIDDLE, 0) => (CGEventType::OtherMouseUp, CGMouseButton::Center),
(b, 1) if b == input_event::BTN_LEFT => {
(CGEventType::LeftMouseDown, CGMouseButton::Left)
}
(b, 0) if b == input_event::BTN_LEFT => {
(CGEventType::LeftMouseUp, CGMouseButton::Left)
}
(b, 1) if b == input_event::BTN_RIGHT => {
(CGEventType::RightMouseDown, CGMouseButton::Right)
}
(b, 0) if b == input_event::BTN_RIGHT => {
(CGEventType::RightMouseUp, CGMouseButton::Right)
}
(b, 1) if b == input_event::BTN_MIDDLE => {
(CGEventType::OtherMouseDown, CGMouseButton::Center)
}
(b, 0) if b == input_event::BTN_MIDDLE => {
(CGEventType::OtherMouseUp, CGMouseButton::Center)
}
_ => {
log::warn!("invalid button event: {button},{state}");
return Ok(());
@@ -304,22 +297,6 @@ impl Emulation for MacOSEmulation {
// store button state
self.button_state[mouse_button] = state == 1;
// update previous button state
if state == 1 {
if self.previous_button.is_some_and(|b| b.eq(&mouse_button))
&& self
.previous_button_click
.is_some_and(|i| i.elapsed() < DOUBLE_CLICK_INTERVAL)
{
self.button_click_state += 1;
} else {
self.button_click_state = 1;
}
self.previous_button = Some(mouse_button);
self.previous_button_click = Some(Instant::now());
}
log::debug!("click_state: {}", self.button_click_state);
let location = self.get_mouse_location().unwrap();
let event = match CGEvent::new_mouse_event(
self.event_source.clone(),
@@ -333,10 +310,6 @@ impl Emulation for MacOSEmulation {
return Ok(());
}
};
event.set_integer_value_field(
EventField::MOUSE_EVENT_CLICK_STATE,
self.button_click_state,
);
event.post(CGEventTapLocation::HID);
}
PointerEvent::Axis {
@@ -370,10 +343,9 @@ impl Emulation for MacOSEmulation {
event.post(CGEventTapLocation::HID);
}
PointerEvent::AxisDiscrete120 { axis, value } => {
const LINES_PER_STEP: i32 = 3;
let (count, wheel1, wheel2, wheel3) = match axis {
0 => (1, value / (120 / LINES_PER_STEP), 0, 0), // 0 = vertical => 1 scroll wheel device (y axis)
1 => (2, 0, value / (120 / LINES_PER_STEP), 0), // 1 = horizontal => 2 scroll wheel devices (y, x) -> (0, x)
0 => (1, value, 0, 0), // 0 = vertical => 1 scroll wheel device (y axis)
1 => (2, 0, value, 0), // 1 = horizontal => 2 scroll wheel devices (y, x) -> (0, x)
_ => {
log::warn!("invalid scroll event: {axis}, {value}");
return Ok(());
@@ -381,7 +353,7 @@ impl Emulation for MacOSEmulation {
};
let event = match CGEvent::new_scroll_event(
self.event_source.clone(),
ScrollEventUnit::LINE,
ScrollEventUnit::PIXEL,
count,
wheel1,
wheel2,
@@ -409,12 +381,18 @@ impl Emulation for MacOSEmulation {
return Ok(());
}
};
update_modifiers(&self.modifier_state, key, state);
match state {
// pressed
1 => self.spawn_repeat_task(code).await,
_ => self.cancel_repeat_task().await,
}
update_modifiers(&self.modifier_state, key, state);
key_event(
self.event_source.clone(),
code,
state,
self.modifier_state.get(),
);
}
KeyboardEvent::Modifiers {
depressed,
@@ -438,21 +416,6 @@ impl Emulation for MacOSEmulation {
async fn terminate(&mut self) {}
}
trait ButtonEq {
fn eq(&self, other: &Self) -> bool;
}
impl ButtonEq for CGMouseButton {
fn eq(&self, other: &Self) -> bool {
matches!(
(self, other),
(CGMouseButton::Left, CGMouseButton::Left)
| (CGMouseButton::Right, CGMouseButton::Right)
| (CGMouseButton::Center, CGMouseButton::Center)
)
}
}
fn update_modifiers(modifiers: &Cell<XMods>, key: u32, state: u8) -> bool {
if let Ok(key) = scancode::Linux::try_from(key) {
let mask = match key {

View File

@@ -2,17 +2,14 @@ use crate::error::EmulationError;
use super::{error::WlrootsEmulationCreationError, Emulation};
use async_trait::async_trait;
use bitflags::bitflags;
use std::collections::HashMap;
use std::io;
use std::os::fd::{AsFd, OwnedFd};
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
use wayland_client::backend::WaylandError;
use wayland_client::WEnum;
use wayland_client::protocol::wl_keyboard::{self, WlKeyboard};
use wayland_client::protocol::wl_pointer::{Axis, AxisSource, ButtonState};
use wayland_client::protocol::wl_pointer::{Axis, ButtonState};
use wayland_client::protocol::wl_seat::WlSeat;
use wayland_protocols_wlr::virtual_pointer::v1::client::{
zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1 as VpManager,
@@ -31,7 +28,7 @@ use wayland_client::{
Connection, Dispatch, EventQueue, QueueHandle,
};
use input_event::{scancode, Event, KeyboardEvent, PointerEvent};
use input_event::{Event, KeyboardEvent, PointerEvent};
use super::error::WaylandBindError;
use super::EmulationHandle;
@@ -105,11 +102,7 @@ impl State {
panic!("no keymap");
}
let vinput = VirtualInput {
pointer,
keyboard,
modifiers: Arc::new(Mutex::new(XMods::empty())),
};
let vinput = VirtualInput { pointer, keyboard };
self.input_for_client.insert(client, vinput);
}
@@ -163,13 +156,13 @@ impl Emulation for WlrootsEmulation {
async fn create(&mut self, handle: EmulationHandle) {
self.state.add_client(handle);
if let Err(e) = self.queue.flush() {
log::error!("{e}");
log::error!("{}", e);
}
}
async fn destroy(&mut self, handle: EmulationHandle) {
self.state.destroy_client(handle);
if let Err(e) = self.queue.flush() {
log::error!("{e}");
log::error!("{}", e);
}
}
async fn terminate(&mut self) {
@@ -180,16 +173,10 @@ impl Emulation for WlrootsEmulation {
struct VirtualInput {
pointer: Vp,
keyboard: Vk,
modifiers: Arc<Mutex<XMods>>,
}
impl VirtualInput {
fn consume_event(&self, event: Event) -> Result<(), ()> {
let now: u32 = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u32;
match event {
Event::Pointer(e) => {
match e {
@@ -210,8 +197,7 @@ impl VirtualInput {
PointerEvent::AxisDiscrete120 { axis, value } => {
let axis: Axis = (axis as u32).try_into()?;
self.pointer
.axis_discrete(now, axis, value as f64 / 8., value / 120);
self.pointer.axis_source(AxisSource::Wheel);
.axis_discrete(0, axis, value as f64 / 6., value / 120);
self.pointer.frame();
}
}
@@ -220,17 +206,6 @@ impl VirtualInput {
Event::Keyboard(e) => match e {
KeyboardEvent::Key { time, key, state } => {
self.keyboard.key(time, key, state as u32);
if let Ok(mut mods) = self.modifiers.lock() {
if mods.update_by_key_event(key, state) {
log::trace!("Key triggers modifier change: {mods:?}");
self.keyboard.modifiers(
mods.mask_pressed().bits(),
0,
mods.mask_locks().bits(),
0,
);
}
}
}
KeyboardEvent::Modifiers {
depressed: mods_depressed,
@@ -238,10 +213,6 @@ impl VirtualInput {
locked: mods_locked,
group,
} => {
// Synchronize internal modifier state, assuming server is authoritative
if let Ok(mut mods) = self.modifiers.lock() {
mods.update_by_mods_event(e);
}
self.keyboard
.modifiers(mods_depressed, mods_latched, mods_locked, group);
}
@@ -302,74 +273,3 @@ impl Dispatch<WlSeat, ()> for State {
}
}
}
// From X11/X.h
bitflags! {
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
struct XMods: u32 {
const ShiftMask = (1<<0);
const LockMask = (1<<1);
const ControlMask = (1<<2);
const Mod1Mask = (1<<3);
const Mod2Mask = (1<<4);
const Mod3Mask = (1<<5);
const Mod4Mask = (1<<6);
const Mod5Mask = (1<<7);
}
}
impl XMods {
fn update_by_mods_event(&mut self, evt: KeyboardEvent) {
if let KeyboardEvent::Modifiers {
depressed, locked, ..
} = evt
{
*self = XMods::from_bits_truncate(depressed) | XMods::from_bits_truncate(locked);
}
}
fn update_by_key_event(&mut self, key: u32, state: u8) -> bool {
if let Ok(key) = scancode::Linux::try_from(key) {
log::trace!("Attempting to process modifier from: {key:#?}");
let pressed_mask = match key {
scancode::Linux::KeyLeftShift | scancode::Linux::KeyRightShift => XMods::ShiftMask,
scancode::Linux::KeyLeftCtrl | scancode::Linux::KeyRightCtrl => XMods::ControlMask,
scancode::Linux::KeyLeftAlt | scancode::Linux::KeyRightalt => XMods::Mod1Mask,
scancode::Linux::KeyLeftMeta | scancode::Linux::KeyRightmeta => XMods::Mod4Mask,
_ => XMods::empty(),
};
let locked_mask = match key {
scancode::Linux::KeyCapsLock => XMods::LockMask,
scancode::Linux::KeyNumlock => XMods::Mod2Mask,
scancode::Linux::KeyScrollLock => XMods::Mod3Mask,
_ => XMods::empty(),
};
// unchanged
if pressed_mask.is_empty() && locked_mask.is_empty() {
log::trace!("{key:#?} is not a modifier key");
return false;
}
match state {
1 => self.insert(pressed_mask),
_ => {
self.remove(pressed_mask);
self.toggle(locked_mask);
}
}
true
} else {
false
}
}
fn mask_locks(&self) -> XMods {
*self & (XMods::LockMask | XMods::Mod2Mask | XMods::Mod3Mask)
}
fn mask_pressed(&self) -> XMods {
*self & (XMods::ShiftMask | XMods::ControlMask | XMods::Mod1Mask | XMods::Mod4Mask)
}
}

View File

@@ -23,7 +23,7 @@ impl X11Emulation {
pub(crate) fn new() -> Result<Self, X11EmulationCreationError> {
let display = unsafe {
match xlib::XOpenDisplay(ptr::null()) {
d if std::ptr::eq(d, ptr::null_mut::<xlib::Display>()) => {
d if d == ptr::null::<xlib::Display>() as *mut xlib::Display => {
Err(X11EmulationCreationError::OpenDisplay)
}
display => Ok(display),

View File

@@ -52,7 +52,7 @@ impl<'a> DesktopPortalEmulation<'a> {
}
#[async_trait]
impl Emulation for DesktopPortalEmulation<'_> {
impl<'a> Emulation for DesktopPortalEmulation<'a> {
async fn consume(
&mut self,
event: input_event::Event,
@@ -141,8 +141,9 @@ impl Emulation for DesktopPortalEmulation<'_> {
}
}
impl AsyncDrop for DesktopPortalEmulation<'_> {
impl<'a> AsyncDrop for DesktopPortalEmulation<'a> {
#[doc = r" Perform the async cleanup."]
#[must_use]
#[allow(clippy::type_complexity, clippy::type_repetition_in_bounds)]
fn async_drop<'async_trait>(
self,

View File

@@ -14,7 +14,7 @@ serde = { version = "1.0", features = ["derive"] }
thiserror = "2.0.0"
[target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
reis = { version = "0.5.0", optional = true }
reis = { version = "0.4", optional = true }
[features]
default = ["libei"]

View File

@@ -112,8 +112,8 @@ impl Display for KeyboardEvent {
impl Display for Event {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Event::Pointer(p) => write!(f, "{p}"),
Event::Keyboard(k) => write!(f, "{k}"),
Event::Pointer(p) => write!(f, "{}", p),
Event::Keyboard(k) => write!(f, "{}", k),
}
}
}

View File

@@ -9,8 +9,6 @@ repository = "https://github.com/feschber/lan-mouse"
[dependencies]
futures = "0.3.30"
lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.2.0" }
clap = { version = "4.4.11", features = ["derive"] }
thiserror = "2.0.0"
tokio = { version = "1.32.0", features = [
"io-util",
"io-std",

View File

@@ -0,0 +1,153 @@
use std::{
fmt::Display,
str::{FromStr, SplitWhitespace},
};
use lan_mouse_ipc::{ClientHandle, Position};
pub(super) enum CommandType {
NoCommand,
Help,
Connect,
Disconnect,
Activate,
Deactivate,
List,
SetHost,
SetPort,
}
#[derive(Debug)]
pub(super) struct InvalidCommand {
cmd: String,
}
impl Display for InvalidCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "invalid command: \"{}\"", self.cmd)
}
}
impl FromStr for CommandType {
type Err = InvalidCommand;
fn from_str(s: &str) -> std::prelude::v1::Result<Self, Self::Err> {
match s {
"connect" => Ok(Self::Connect),
"disconnect" => Ok(Self::Disconnect),
"activate" => Ok(Self::Activate),
"deactivate" => Ok(Self::Deactivate),
"list" => Ok(Self::List),
"set-host" => Ok(Self::SetHost),
"set-port" => Ok(Self::SetPort),
"help" => Ok(Self::Help),
_ => Err(InvalidCommand { cmd: s.to_string() }),
}
}
}
#[derive(Debug)]
pub(super) enum Command {
None,
Help,
Connect(Position, String, Option<u16>),
Disconnect(ClientHandle),
Activate(ClientHandle),
Deactivate(ClientHandle),
List,
SetHost(ClientHandle, String),
SetPort(ClientHandle, Option<u16>),
}
impl CommandType {
pub(super) fn usage(&self) -> &'static str {
match self {
CommandType::Help => "help",
CommandType::NoCommand => "",
CommandType::Connect => "connect left|right|top|bottom <host> [<port>]",
CommandType::Disconnect => "disconnect <id>",
CommandType::Activate => "activate <id>",
CommandType::Deactivate => "deactivate <id>",
CommandType::List => "list",
CommandType::SetHost => "set-host <id> <host>",
CommandType::SetPort => "set-port <id> <host>",
}
}
}
pub(super) enum CommandParseError {
Usage(CommandType),
Invalid(InvalidCommand),
}
impl Display for CommandParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Usage(cmd) => write!(f, "usage: {}", cmd.usage()),
Self::Invalid(cmd) => write!(f, "{}", cmd),
}
}
}
impl FromStr for Command {
type Err = CommandParseError;
fn from_str(cmd: &str) -> Result<Self, Self::Err> {
let mut args = cmd.split_whitespace();
let cmd_type: CommandType = match args.next() {
Some(c) => c.parse().map_err(CommandParseError::Invalid),
None => Ok(CommandType::NoCommand),
}?;
match cmd_type {
CommandType::Help => Ok(Command::Help),
CommandType::NoCommand => Ok(Command::None),
CommandType::Connect => parse_connect_cmd(args),
CommandType::Disconnect => parse_disconnect_cmd(args),
CommandType::Activate => parse_activate_cmd(args),
CommandType::Deactivate => parse_deactivate_cmd(args),
CommandType::List => Ok(Command::List),
CommandType::SetHost => parse_set_host(args),
CommandType::SetPort => parse_set_port(args),
}
}
}
fn parse_connect_cmd(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::Connect);
let pos = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
let host = args.next().ok_or(USAGE)?.to_string();
let port = args.next().and_then(|p| p.parse().ok());
Ok(Command::Connect(pos, host, port))
}
fn parse_disconnect_cmd(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::Disconnect);
let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
Ok(Command::Disconnect(id))
}
fn parse_activate_cmd(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::Activate);
let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
Ok(Command::Activate(id))
}
fn parse_deactivate_cmd(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::Deactivate);
let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
Ok(Command::Deactivate(id))
}
fn parse_set_host(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::SetHost);
let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
let host = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
Ok(Command::SetHost(id, host))
}
fn parse_set_port(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::SetPort);
let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
let port = args.next().and_then(|p| p.parse().ok());
Ok(Command::SetPort(id, port))
}

View File

@@ -1,167 +1,320 @@
use clap::{Args, Parser, Subcommand};
use futures::StreamExt;
use std::{net::IpAddr, time::Duration};
use thiserror::Error;
use lan_mouse_ipc::{
connect_async, ClientHandle, ConnectionError, FrontendEvent, FrontendRequest, IpcError,
Position,
use tokio::{
io::{AsyncBufReadExt, BufReader},
task::LocalSet,
};
#[derive(Debug, Error)]
pub enum CliError {
/// is the service running?
#[error("could not connect: `{0}` - is the service running?")]
ServiceNotRunning(#[from] ConnectionError),
#[error("error communicating with service: {0}")]
Ipc(#[from] IpcError),
}
use std::io::{self, Write};
#[derive(Parser, Clone, Debug, PartialEq, Eq)]
#[command(name = "lan-mouse-cli", about = "LanMouse CLI interface")]
pub struct CliArgs {
#[command(subcommand)]
command: CliSubcommand,
}
use self::command::{Command, CommandType};
#[derive(Args, Clone, Debug, PartialEq, Eq)]
struct Client {
#[arg(long)]
hostname: Option<String>,
#[arg(long)]
port: Option<u16>,
#[arg(long)]
ips: Option<Vec<IpAddr>>,
#[arg(long)]
enter_hook: Option<String>,
}
use lan_mouse_ipc::{
AsyncFrontendEventReader, AsyncFrontendRequestWriter, ClientConfig, ClientHandle, ClientState,
FrontendEvent, FrontendRequest, IpcError, DEFAULT_PORT,
};
#[derive(Clone, Subcommand, Debug, PartialEq, Eq)]
enum CliSubcommand {
/// add a new client
AddClient(Client),
/// remove an existing client
RemoveClient { id: ClientHandle },
/// activate a client
Activate { id: ClientHandle },
/// deactivate a client
Deactivate { id: ClientHandle },
/// list configured clients
List,
/// change hostname
SetHost {
id: ClientHandle,
host: Option<String>,
},
/// change port
SetPort { id: ClientHandle, port: u16 },
/// set position
SetPosition { id: ClientHandle, pos: Position },
/// set ips
SetIps { id: ClientHandle, ips: Vec<IpAddr> },
/// re-enable capture
EnableCapture,
/// re-enable emulation
EnableEmulation,
/// authorize a public key
AuthorizeKey {
description: String,
sha256_fingerprint: String,
},
/// deauthorize a public key
RemoveAuthorizedKey { sha256_fingerprint: String },
}
mod command;
pub async fn run(args: CliArgs) -> Result<(), CliError> {
execute(args.command).await?;
pub fn run() -> Result<(), IpcError> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()?;
runtime.block_on(LocalSet::new().run_until(async move {
let (rx, tx) = lan_mouse_ipc::connect_async().await?;
let mut cli = Cli::new(rx, tx);
cli.run().await
}))?;
Ok(())
}
async fn execute(cmd: CliSubcommand) -> Result<(), CliError> {
let (mut rx, mut tx) = connect_async(Some(Duration::from_millis(500))).await?;
match cmd {
CliSubcommand::AddClient(Client {
hostname,
port,
ips,
enter_hook,
}) => {
tx.request(FrontendRequest::Create).await?;
while let Some(e) = rx.next().await {
if let FrontendEvent::Created(handle, _, _) = e? {
if let Some(hostname) = hostname {
tx.request(FrontendRequest::UpdateHostname(handle, Some(hostname)))
.await?;
}
if let Some(port) = port {
tx.request(FrontendRequest::UpdatePort(handle, port))
.await?;
}
if let Some(ips) = ips {
tx.request(FrontendRequest::UpdateFixIps(handle, ips))
.await?;
}
if let Some(enter_hook) = enter_hook {
tx.request(FrontendRequest::UpdateEnterHook(handle, Some(enter_hook)))
.await?;
}
break;
}
}
}
CliSubcommand::RemoveClient { id } => tx.request(FrontendRequest::Delete(id)).await?,
CliSubcommand::Activate { id } => tx.request(FrontendRequest::Activate(id, true)).await?,
CliSubcommand::Deactivate { id } => {
tx.request(FrontendRequest::Activate(id, false)).await?
}
CliSubcommand::List => {
tx.request(FrontendRequest::Enumerate()).await?;
while let Some(e) = rx.next().await {
if let FrontendEvent::Enumerate(clients) = e? {
for (handle, config, state) in clients {
let host = config.hostname.unwrap_or("unknown".to_owned());
let port = config.port;
let pos = config.pos;
let active = state.active;
let ips = state.ips;
println!(
"id {handle}: {host}:{port} ({pos}) active: {active}, ips: {ips:?}"
);
}
break;
}
}
}
CliSubcommand::SetHost { id, host } => {
tx.request(FrontendRequest::UpdateHostname(id, host))
.await?
}
CliSubcommand::SetPort { id, port } => {
tx.request(FrontendRequest::UpdatePort(id, port)).await?
}
CliSubcommand::SetPosition { id, pos } => {
tx.request(FrontendRequest::UpdatePosition(id, pos)).await?
}
CliSubcommand::SetIps { id, ips } => {
tx.request(FrontendRequest::UpdateFixIps(id, ips)).await?
}
CliSubcommand::EnableCapture => tx.request(FrontendRequest::EnableCapture).await?,
CliSubcommand::EnableEmulation => tx.request(FrontendRequest::EnableEmulation).await?,
CliSubcommand::AuthorizeKey {
description,
sha256_fingerprint,
} => {
tx.request(FrontendRequest::AuthorizeKey(
description,
sha256_fingerprint,
))
.await?
}
CliSubcommand::RemoveAuthorizedKey { sha256_fingerprint } => {
tx.request(FrontendRequest::RemoveAuthorizedKey(sha256_fingerprint))
.await?
struct Cli {
clients: Vec<(ClientHandle, ClientConfig, ClientState)>,
changed: Option<ClientHandle>,
rx: AsyncFrontendEventReader,
tx: AsyncFrontendRequestWriter,
}
impl Cli {
fn new(rx: AsyncFrontendEventReader, tx: AsyncFrontendRequestWriter) -> Cli {
Self {
clients: vec![],
changed: None,
rx,
tx,
}
}
async fn run(&mut self) -> Result<(), IpcError> {
let stdin = tokio::io::stdin();
let stdin = BufReader::new(stdin);
let mut stdin = stdin.lines();
/* initial state sync */
self.clients = loop {
match self.rx.next().await {
Some(Ok(e)) => {
if let FrontendEvent::Enumerate(clients) = e {
break clients;
}
}
Some(Err(e)) => return Err(e),
None => return Ok(()),
}
};
loop {
prompt()?;
tokio::select! {
line = stdin.next_line() => {
let Some(line) = line? else {
break Ok(());
};
let cmd: Command = match line.parse() {
Ok(cmd) => cmd,
Err(e) => {
eprintln!("{e}");
continue;
}
};
self.execute(cmd).await?;
}
event = self.rx.next() => {
if let Some(event) = event {
self.handle_event(event?);
} else {
break Ok(());
}
}
}
if let Some(handle) = self.changed.take() {
self.update_client(handle).await?;
}
}
}
async fn update_client(&mut self, handle: ClientHandle) -> Result<(), IpcError> {
self.tx.request(FrontendRequest::GetState(handle)).await?;
while let Some(Ok(event)) = self.rx.next().await {
self.handle_event(event.clone());
if let FrontendEvent::State(_, _, _) | FrontendEvent::NoSuchClient(_) = event {
break;
}
}
Ok(())
}
async fn execute(&mut self, cmd: Command) -> Result<(), IpcError> {
match cmd {
Command::None => {}
Command::Connect(pos, host, port) => {
let request = FrontendRequest::Create;
self.tx.request(request).await?;
let handle = loop {
if let Some(Ok(event)) = self.rx.next().await {
match event {
FrontendEvent::Created(h, c, s) => {
self.clients.push((h, c, s));
break h;
}
_ => {
self.handle_event(event);
continue;
}
}
}
};
for request in [
FrontendRequest::UpdateHostname(handle, Some(host.clone())),
FrontendRequest::UpdatePort(handle, port.unwrap_or(DEFAULT_PORT)),
FrontendRequest::UpdatePosition(handle, pos),
] {
self.tx.request(request).await?;
}
self.update_client(handle).await?;
}
Command::Disconnect(id) => {
self.tx.request(FrontendRequest::Delete(id)).await?;
loop {
if let Some(Ok(event)) = self.rx.next().await {
self.handle_event(event.clone());
if let FrontendEvent::Deleted(_) = event {
self.handle_event(event);
break;
}
}
}
}
Command::Activate(id) => {
self.tx.request(FrontendRequest::Activate(id, true)).await?;
self.update_client(id).await?;
}
Command::Deactivate(id) => {
self.tx
.request(FrontendRequest::Activate(id, false))
.await?;
self.update_client(id).await?;
}
Command::List => {
self.tx.request(FrontendRequest::Enumerate()).await?;
while let Some(e) = self.rx.next().await {
let event = e?;
self.handle_event(event.clone());
if let FrontendEvent::Enumerate(_) = event {
break;
}
}
}
Command::SetHost(handle, host) => {
let request = FrontendRequest::UpdateHostname(handle, Some(host.clone()));
self.tx.request(request).await?;
self.update_client(handle).await?;
}
Command::SetPort(handle, port) => {
let request = FrontendRequest::UpdatePort(handle, port.unwrap_or(DEFAULT_PORT));
self.tx.request(request).await?;
self.update_client(handle).await?;
}
Command::Help => {
for cmd_type in [
CommandType::List,
CommandType::Connect,
CommandType::Disconnect,
CommandType::Activate,
CommandType::Deactivate,
CommandType::SetHost,
CommandType::SetPort,
] {
eprintln!("{}", cmd_type.usage());
}
}
}
Ok(())
}
fn find_mut(
&mut self,
handle: ClientHandle,
) -> Option<&mut (ClientHandle, ClientConfig, ClientState)> {
self.clients.iter_mut().find(|(h, _, _)| *h == handle)
}
fn remove(
&mut self,
handle: ClientHandle,
) -> Option<(ClientHandle, ClientConfig, ClientState)> {
let idx = self.clients.iter().position(|(h, _, _)| *h == handle);
idx.map(|i| self.clients.swap_remove(i))
}
fn handle_event(&mut self, event: FrontendEvent) {
match event {
FrontendEvent::Changed(h) => self.changed = Some(h),
FrontendEvent::Created(h, c, s) => {
eprint!("client added ({h}): ");
print_config(&c);
eprint!(" ");
print_state(&s);
eprintln!();
self.clients.push((h, c, s));
}
FrontendEvent::NoSuchClient(h) => {
eprintln!("no such client: {h}");
}
FrontendEvent::State(h, c, s) => {
if let Some((_, config, state)) = self.find_mut(h) {
let old_host = config.hostname.clone().unwrap_or("\"\"".into());
let new_host = c.hostname.clone().unwrap_or("\"\"".into());
if old_host != new_host {
eprintln!(
"client {h}: hostname updated ({} -> {})",
old_host, new_host
);
}
if config.port != c.port {
eprintln!("client {h} changed port: {} -> {}", config.port, c.port);
}
if config.fix_ips != c.fix_ips {
eprintln!("client {h} ips updated: {:?}", c.fix_ips)
}
*config = c;
if state.active ^ s.active {
eprintln!(
"client {h} {}",
if s.active { "activated" } else { "deactivated" }
);
}
*state = s;
}
}
FrontendEvent::Deleted(h) => {
if let Some((h, c, _)) = self.remove(h) {
eprint!("client {h} removed (");
print_config(&c);
eprintln!(")");
}
}
FrontendEvent::PortChanged(p, e) => {
if let Some(e) = e {
eprintln!("failed to change port: {e}");
} else {
eprintln!("changed port to {p}");
}
}
FrontendEvent::Enumerate(clients) => {
self.clients = clients;
self.print_clients();
}
FrontendEvent::Error(e) => {
eprintln!("ERROR: {e}");
}
FrontendEvent::CaptureStatus(s) => {
eprintln!("capture status: {s:?}")
}
FrontendEvent::EmulationStatus(s) => {
eprintln!("emulation status: {s:?}")
}
FrontendEvent::AuthorizedUpdated(fingerprints) => {
eprintln!("authorized keys changed:");
for (desc, fp) in fingerprints {
eprintln!("{desc}: {fp}");
}
}
FrontendEvent::PublicKeyFingerprint(fp) => {
eprintln!("the public key fingerprint of this device is {fp}");
}
FrontendEvent::IncomingConnected(..) => {}
FrontendEvent::IncomingDisconnected(..) => {}
}
}
fn print_clients(&mut self) {
for (h, c, s) in self.clients.iter() {
eprint!("client {h}: ");
print_config(c);
eprint!(" ");
print_state(s);
eprintln!();
}
}
}
fn prompt() -> io::Result<()> {
eprint!("lan-mouse > ");
std::io::stderr().flush()?;
Ok(())
}
fn print_config(c: &ClientConfig) {
eprint!(
"{}:{} ({}), ips: {:?}",
c.hostname.clone().unwrap_or("(no hostname)".into()),
c.port,
c.pos,
c.fix_ips
);
}
fn print_state(s: &ClientState) {
eprint!("active: {}, dns: {:?}", s.active, s.ips);
}

View File

@@ -13,7 +13,6 @@ async-channel = { version = "2.1.1" }
hostname = "0.4.0"
log = "0.4.20"
lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.2.0" }
thiserror = "2.0.0"
[build-dependencies]
glib-build-tools = { version = "0.20.0" }

View File

@@ -1,102 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<requires lib="libadwaita" version="1.0"/>
<template class="AuthorizationWindow" parent="AdwWindow">
<property name="modal">True</property>
<property name="width-request">180</property>
<property name="default-width">180</property>
<property name="height-request">180</property>
<property name="default-height">180</property>
<property name="title" translatable="yes">Unauthorized Device</property>
<property name="content">
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="vexpand">True</property>
<child type="top">
<object class="AdwHeaderBar">
<style>
<class name="flat"/>
</style>
</object>
</child>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">30</property>
<property name="margin-start">30</property>
<property name="margin-end">30</property>
<property name="margin-top">30</property>
<property name="margin-bottom">30</property>
<child>
<object class="GtkLabel">
<property name="label">An unauthorized Device is trying to connect. Do you want to authorize this Device?</property>
<property name="width-request">100</property>
<property name="wrap">word-wrap</property>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<property name="title">sha256 fingerprint</property>
<child>
<object class="AdwActionRow">
<property name="child">
<object class="GtkLabel" id="fingerprint">
<property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
<property name="vexpand">True</property>
<property name="hexpand">False</property>
<property name="wrap">True</property>
<property name="wrap-mode">word-char</property>
<property name="justify">center</property>
<property name="xalign">0.5</property>
<property name="margin-top">10</property>
<property name="margin-bottom">10</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="width-chars">64</property>
</object>
</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="margin-start">30</property>
<property name="margin-end">30</property>
<property name="margin-top">30</property>
<property name="margin-bottom">30</property>
<property name="orientation">horizontal</property>
<property name="spacing">30</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="valign">end</property>
<child>
<object class="GtkButton" id="cancel_button">
<signal name="clicked" handler="handle_cancel" swapped="true"/>
<property name="label" translatable="yes">Cancel</property>
<property name="can-shrink">True</property>
<property name="height-request">50</property>
<property name="hexpand">True</property>
</object>
</child>
<child>
<object class="GtkButton" id="confirm_button">
<signal name="clicked" handler="handle_confirm" swapped="true"/>
<property name="label" translatable="yes">Authorize</property>
<property name="can-shrink">True</property>
<property name="height-request">50</property>
<property name="hexpand">True</property>
<style>
<class name="destructive-action"/>
</style>
</object>
</child>
</object>
</child>
</object>
</property>
</template>
</interface>

View File

@@ -5,6 +5,7 @@
<!-- enabled -->
<child type="prefix">
<object class="GtkSwitch" id="enable_switch">
<signal name="state_set" handler="handle_client_set_state" swapped="true"/>
<property name="valign">center</property>
<property name="halign">end</property>
<property name="tooltip-text" translatable="yes">enable</property>

View File

@@ -2,7 +2,6 @@
<gresources>
<gresource prefix="/de/feschber/LanMouse">
<file compressed="true" preprocess="xml-stripblanks">window.ui</file>
<file compressed="true" preprocess="xml-stripblanks">authorization_window.ui</file>
<file compressed="true" preprocess="xml-stripblanks">fingerprint_window.ui</file>
<file compressed="true" preprocess="xml-stripblanks">client_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">key_row.ui</file>

View File

@@ -1,19 +0,0 @@
mod imp;
use glib::Object;
use gtk::{gio, glib, subclass::prelude::ObjectSubclassIsExt};
glib::wrapper! {
pub struct AuthorizationWindow(ObjectSubclass<imp::AuthorizationWindow>)
@extends adw::Window, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl AuthorizationWindow {
pub(crate) fn new(fingerprint: &str) -> Self {
let window: Self = Object::builder().build();
window.imp().set_fingerprint(fingerprint);
window
}
}

View File

@@ -1,75 +0,0 @@
use std::sync::OnceLock;
use adw::prelude::*;
use adw::subclass::prelude::*;
use glib::subclass::InitializingObject;
use gtk::{
glib::{self, subclass::Signal},
template_callbacks, Button, CompositeTemplate, Label,
};
#[derive(CompositeTemplate, Default)]
#[template(resource = "/de/feschber/LanMouse/authorization_window.ui")]
pub struct AuthorizationWindow {
#[template_child]
pub fingerprint: TemplateChild<Label>,
#[template_child]
pub cancel_button: TemplateChild<Button>,
#[template_child]
pub confirm_button: TemplateChild<Button>,
}
#[glib::object_subclass]
impl ObjectSubclass for AuthorizationWindow {
const NAME: &'static str = "AuthorizationWindow";
const ABSTRACT: bool = false;
type Type = super::AuthorizationWindow;
type ParentType = adw::Window;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
klass.bind_template_callbacks();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[template_callbacks]
impl AuthorizationWindow {
#[template_callback]
fn handle_confirm(&self, _button: Button) {
let fp = self.fingerprint.text().as_str().trim().to_owned();
self.obj().emit_by_name("confirm-clicked", &[&fp])
}
#[template_callback]
fn handle_cancel(&self, _: Button) {
self.obj().emit_by_name("cancel-clicked", &[])
}
pub(super) fn set_fingerprint(&self, fingerprint: &str) {
self.fingerprint.set_text(fingerprint);
}
}
impl ObjectImpl for AuthorizationWindow {
fn signals() -> &'static [Signal] {
static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
SIGNALS.get_or_init(|| {
vec![
Signal::builder("confirm-clicked")
.param_types([String::static_type()])
.build(),
Signal::builder("cancel-clicked").build(),
]
})
}
}
impl WidgetImpl for AuthorizationWindow {}
impl WindowImpl for AuthorizationWindow {}
impl ApplicationWindowImpl for AuthorizationWindow {}
impl AdwWindowImpl for AuthorizationWindow {}

View File

@@ -13,7 +13,7 @@ use super::ClientData;
#[properties(wrapper_type = super::ClientObject)]
pub struct ClientObject {
#[property(name = "handle", get, set, type = ClientHandle, member = handle)]
#[property(name = "hostname", get, set, type = Option<String>, member = hostname)]
#[property(name = "hostname", get, set, type = String, member = hostname)]
#[property(name = "port", get, set, type = u32, member = port, maximum = u16::MAX as u32)]
#[property(name = "active", get, set, type = bool, member = active)]
#[property(name = "position", get, set, type = String, member = position)]

View File

@@ -4,7 +4,7 @@ use adw::prelude::*;
use adw::subclass::prelude::*;
use gtk::glib::{self, Object};
use lan_mouse_ipc::{Position, DEFAULT_PORT};
use lan_mouse_ipc::DEFAULT_PORT;
use super::ClientObject;
@@ -15,32 +15,25 @@ glib::wrapper! {
}
impl ClientRow {
pub fn new(client_object: &ClientObject) -> Self {
let client_row: Self = Object::builder().build();
client_row
.imp()
.client_object
.borrow_mut()
.replace(client_object.clone());
client_row
pub fn new(_client_object: &ClientObject) -> Self {
Object::builder().build()
}
pub fn bind(&self, client_object: &ClientObject) {
let mut bindings = self.imp().bindings.borrow_mut();
// bind client active to switch state
let active_binding = client_object
.bind_property("active", &self.imp().enable_switch.get(), "state")
.bidirectional()
.sync_create()
.build();
// bind client active to switch position
let switch_position_binding = client_object
.bind_property("active", &self.imp().enable_switch.get(), "active")
.bidirectional()
.sync_create()
.build();
// bind hostname to hostname edit field
let hostname_binding = client_object
.bind_property("hostname", &self.imp().hostname.get(), "text")
.transform_to(|_, v: Option<String>| {
@@ -50,48 +43,72 @@ impl ClientRow {
Some("".to_string())
}
})
.transform_from(|_, v: String| {
if v.as_str().trim() == "" {
Some(None)
} else {
Some(Some(v))
}
})
.bidirectional()
.sync_create()
.build();
// bind hostname to title
let title_binding = client_object
.bind_property("hostname", self, "title")
.transform_to(|_, v: Option<String>| v.or(Some("<span font_style=\"italic\" font_weight=\"light\" foreground=\"darkgrey\">no hostname!</span>".to_string())))
.sync_create()
.build();
// bind port to port edit field
let port_binding = client_object
.bind_property("port", &self.imp().port.get(), "text")
.transform_to(|_, v: u32| {
if v == DEFAULT_PORT as u32 {
Some("".to_string())
.transform_to(|_, v: Option<String>| {
if let Some(hostname) = v {
Some(hostname)
} else {
Some(v.to_string())
Some("<span font_style=\"italic\" font_weight=\"light\" foreground=\"darkgrey\">no hostname!</span>".to_string())
}
})
.sync_create()
.build();
// bind port to subtitle
let port_binding = client_object
.bind_property("port", &self.imp().port.get(), "text")
.transform_from(|_, v: String| {
if v.is_empty() {
Some(DEFAULT_PORT as u32)
} else {
Some(v.parse::<u16>().unwrap_or(DEFAULT_PORT) as u32)
}
})
.transform_to(|_, v: u32| {
if v == 4242 {
Some("".to_string())
} else {
Some(v.to_string())
}
})
.bidirectional()
.sync_create()
.build();
let subtitle_binding = client_object
.bind_property("port", self, "subtitle")
.sync_create()
.build();
// bind position to selected position
let position_binding = client_object
.bind_property("position", &self.imp().position.get(), "selected")
.transform_from(|_, v: u32| match v {
1 => Some("right"),
2 => Some("top"),
3 => Some("bottom"),
_ => Some("left"),
})
.transform_to(|_, v: String| match v.as_str() {
"right" => Some(1u32),
"right" => Some(1),
"top" => Some(2u32),
"bottom" => Some(3u32),
_ => Some(0u32),
})
.bidirectional()
.sync_create()
.build();
// bind resolving status to spinner visibility
let resolve_binding = client_object
.bind_property(
"resolving",
@@ -101,7 +118,6 @@ impl ClientRow {
.sync_create()
.build();
// bind ips to tooltip-text
let ip_binding = client_object
.bind_property("ips", &self.imp().dns_button.get(), "tooltip-text")
.transform_to(|_, ips: Vec<String>| {
@@ -130,24 +146,4 @@ impl ClientRow {
binding.unbind();
}
}
pub fn set_active(&self, active: bool) {
self.imp().set_active(active);
}
pub fn set_hostname(&self, hostname: Option<String>) {
self.imp().set_hostname(hostname);
}
pub fn set_port(&self, port: u16) {
self.imp().set_port(port);
}
pub fn set_position(&self, pos: Position) {
self.imp().set_pos(pos);
}
pub fn set_dns_state(&self, resolved: bool) {
self.imp().set_dns_state(resolved);
}
}

View File

@@ -3,14 +3,11 @@ use std::cell::RefCell;
use adw::subclass::prelude::*;
use adw::{prelude::*, ActionRow, ComboRow};
use glib::{subclass::InitializingObject, Binding};
use gtk::glib::clone;
use gtk::glib::subclass::Signal;
use gtk::glib::{clone, SignalHandlerId};
use gtk::{glib, Button, CompositeTemplate, Entry, Switch};
use lan_mouse_ipc::Position;
use gtk::{glib, Button, CompositeTemplate, Switch};
use std::sync::OnceLock;
use crate::client_object::ClientObject;
#[derive(CompositeTemplate, Default)]
#[template(resource = "/de/feschber/LanMouse/client_row.ui")]
pub struct ClientRow {
@@ -31,11 +28,6 @@ pub struct ClientRow {
#[template_child]
pub dns_loading_indicator: TemplateChild<gtk::Spinner>,
pub bindings: RefCell<Vec<Binding>>,
hostname_change_handler: RefCell<Option<SignalHandlerId>>,
port_change_handler: RefCell<Option<SignalHandlerId>>,
position_change_handler: RefCell<Option<SignalHandlerId>>,
set_state_handler: RefCell<Option<SignalHandlerId>>,
pub client_object: RefCell<Option<ClientObject>>,
}
#[glib::object_subclass]
@@ -67,61 +59,17 @@ impl ObjectImpl for ClientRow {
row.handle_client_delete(button);
}
));
let handler = self.hostname.connect_changed(clone!(
#[weak(rename_to = row)]
self,
move |entry| {
row.handle_hostname_changed(entry);
}
));
self.hostname_change_handler.replace(Some(handler));
let handler = self.port.connect_changed(clone!(
#[weak(rename_to = row)]
self,
move |entry| {
row.handle_port_changed(entry);
}
));
self.port_change_handler.replace(Some(handler));
let handler = self.position.connect_selected_notify(clone!(
#[weak(rename_to = row)]
self,
move |position| {
row.handle_position_changed(position);
}
));
self.position_change_handler.replace(Some(handler));
let handler = self.enable_switch.connect_state_set(clone!(
#[weak(rename_to = row)]
self,
#[upgrade_or]
glib::Propagation::Proceed,
move |switch, state| {
row.handle_activate_switch(state, switch);
glib::Propagation::Proceed
}
));
self.set_state_handler.replace(Some(handler));
}
fn signals() -> &'static [glib::subclass::Signal] {
static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
SIGNALS.get_or_init(|| {
vec![
Signal::builder("request-activate")
Signal::builder("request-dns").build(),
Signal::builder("request-update")
.param_types([bool::static_type()])
.build(),
Signal::builder("request-delete").build(),
Signal::builder("request-dns").build(),
Signal::builder("request-hostname-change")
.param_types([String::static_type()])
.build(),
Signal::builder("request-port-change")
.param_types([u32::static_type()])
.build(),
Signal::builder("request-position-change")
.param_types([u32::static_type()])
.build(),
]
})
}
@@ -130,97 +78,22 @@ impl ObjectImpl for ClientRow {
#[gtk::template_callbacks]
impl ClientRow {
#[template_callback]
fn handle_activate_switch(&self, state: bool, _switch: &Switch) -> bool {
self.obj().emit_by_name::<()>("request-activate", &[&state]);
fn handle_client_set_state(&self, state: bool, _switch: &Switch) -> bool {
log::debug!("state change -> requesting update");
self.obj().emit_by_name::<()>("request-update", &[&state]);
true // dont run default handler
}
#[template_callback]
fn handle_request_dns(&self, _: &Button) {
fn handle_request_dns(&self, _: Button) {
self.obj().emit_by_name::<()>("request-dns", &[]);
}
#[template_callback]
fn handle_client_delete(&self, _button: &Button) {
log::debug!("delete button pressed -> requesting delete");
self.obj().emit_by_name::<()>("request-delete", &[]);
}
fn handle_port_changed(&self, port_entry: &Entry) {
if let Ok(port) = port_entry.text().parse::<u16>() {
self.obj()
.emit_by_name::<()>("request-port-change", &[&(port as u32)]);
}
}
fn handle_hostname_changed(&self, hostname_entry: &Entry) {
self.obj()
.emit_by_name::<()>("request-hostname-change", &[&hostname_entry.text()]);
}
fn handle_position_changed(&self, position: &ComboRow) {
self.obj()
.emit_by_name("request-position-change", &[&position.selected()])
}
pub(super) fn set_hostname(&self, hostname: Option<String>) {
let position = self.hostname.position();
let handler = self.hostname_change_handler.borrow();
let handler = handler.as_ref().expect("signal handler");
self.hostname.block_signal(handler);
self.client_object
.borrow_mut()
.as_mut()
.expect("client object")
.set_property("hostname", hostname);
self.hostname.unblock_signal(handler);
self.hostname.set_position(position);
}
pub(super) fn set_port(&self, port: u16) {
let position = self.port.position();
let handler = self.port_change_handler.borrow();
let handler = handler.as_ref().expect("signal handler");
self.port.block_signal(handler);
self.client_object
.borrow_mut()
.as_mut()
.expect("client object")
.set_port(port as u32);
self.port.unblock_signal(handler);
self.port.set_position(position);
}
pub(super) fn set_pos(&self, pos: Position) {
let handler = self.position_change_handler.borrow();
let handler = handler.as_ref().expect("signal handler");
self.position.block_signal(handler);
self.client_object
.borrow_mut()
.as_mut()
.expect("client object")
.set_position(pos.to_string());
self.position.unblock_signal(handler);
}
pub(super) fn set_active(&self, active: bool) {
let handler = self.set_state_handler.borrow();
let handler = handler.as_ref().expect("signal handler");
self.enable_switch.block_signal(handler);
self.client_object
.borrow_mut()
.as_mut()
.expect("client object")
.set_active(active);
self.enable_switch.unblock_signal(handler);
}
pub(super) fn set_dns_state(&self, resolved: bool) {
if resolved {
self.dns_button.set_css_classes(&["success"])
} else {
self.dns_button.set_css_classes(&["warning"])
}
}
}
impl WidgetImpl for ClientRow {}

View File

@@ -1,7 +1,7 @@
mod imp;
use glib::Object;
use gtk::{gio, glib, prelude::ObjectExt, subclass::prelude::ObjectSubclassIsExt};
use gtk::{gio, glib};
glib::wrapper! {
pub struct FingerprintWindow(ObjectSubclass<imp::FingerprintWindow>)
@@ -11,12 +11,8 @@ glib::wrapper! {
}
impl FingerprintWindow {
pub(crate) fn new(fingerprint: Option<String>) -> Self {
pub(crate) fn new() -> Self {
let window: Self = Object::builder().build();
if let Some(fp) = fingerprint {
window.imp().fingerprint.set_property("text", fp);
window.imp().fingerprint.set_property("editable", false);
}
window
}
}

View File

@@ -8,7 +8,7 @@ use super::KeyObject;
glib::wrapper! {
pub struct KeyRow(ObjectSubclass<imp::KeyRow>)
@extends gtk::ListBoxRow, gtk::Widget, adw::PreferencesRow, adw::ActionRow,
@extends gtk::ListBoxRow, gtk::Widget, adw::PreferencesRow, adw::ExpanderRow,
@implements gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget;
}

View File

@@ -1,4 +1,3 @@
mod authorization_window;
mod client_object;
mod client_row;
mod fingerprint_window;
@@ -10,24 +9,18 @@ use std::{env, process, str};
use window::Window;
use lan_mouse_ipc::FrontendEvent;
use lan_mouse_ipc::{FrontendEvent, FrontendRequest};
use adw::Application;
use gtk::{gdk::Display, glib::clone, prelude::*, IconTheme};
use gtk::{
gdk::Display, glib::clone, prelude::*, subclass::prelude::ObjectSubclassIsExt, IconTheme,
};
use gtk::{gio, glib, prelude::ApplicationExt};
use self::client_object::ClientObject;
use self::key_object::KeyObject;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum GtkError {
#[error("gtk frontend exited with non zero exit code: {0}")]
NonZeroExitCode(i32),
}
pub fn run() -> Result<(), GtkError> {
pub fn run() -> glib::ExitCode {
log::debug!("running gtk frontend");
#[cfg(windows)]
let ret = std::thread::Builder::new()
@@ -40,10 +33,13 @@ pub fn run() -> Result<(), GtkError> {
#[cfg(not(windows))]
let ret = gtk_main();
match ret {
glib::ExitCode::SUCCESS => Ok(()),
e => Err(GtkError::NonZeroExitCode(e.value())),
if ret == glib::ExitCode::FAILURE {
log::error!("frontend exited with failure");
} else {
log::info!("frontend exited successfully");
}
ret
}
fn gtk_main() -> glib::ExitCode {
@@ -53,11 +49,7 @@ fn gtk_main() -> glib::ExitCode {
.application_id("de.feschber.LanMouse")
.build();
app.connect_startup(|app| {
load_icons();
setup_actions(app);
setup_menu(app);
});
app.connect_startup(|_| load_icons());
app.connect_activate(build_ui);
let args: Vec<&'static str> = vec![];
@@ -70,33 +62,6 @@ fn load_icons() {
icon_theme.add_resource_path("/de/feschber/LanMouse/icons");
}
// Add application actions
fn setup_actions(app: &adw::Application) {
// Quit action
// This is important on macOS, where users expect a File->Quit action with a Cmd+Q shortcut.
let quit_action = gio::SimpleAction::new("quit", None);
quit_action.connect_activate({
let app = app.clone();
move |_, _| {
app.quit();
}
});
app.add_action(&quit_action);
}
// Set up a global menu
//
// Currently this is used only on macOS
fn setup_menu(app: &adw::Application) {
let menu = gio::Menu::new();
let file_menu = gio::Menu::new();
file_menu.append(Some("Quit"), Some("app.quit"));
menu.append_submenu(Some("_File"), &file_menu);
app.set_menubar(Some(&menu))
}
fn build_ui(app: &Application) {
log::debug!("connecting to lan-mouse-socket");
let (mut frontend_rx, frontend_tx) = match lan_mouse_ipc::connect() {
@@ -131,41 +96,54 @@ fn build_ui(app: &Application) {
loop {
let notify = receiver.recv().await.unwrap_or_else(|_| process::exit(1));
match notify {
FrontendEvent::Created(handle, client, state) => {
window.new_client(handle, client, state)
FrontendEvent::Changed(handle) => {
window.request(FrontendRequest::GetState(handle));
}
FrontendEvent::Created(handle, client, state) => {
window.new_client(handle, client, state);
}
FrontendEvent::Deleted(client) => {
window.delete_client(client);
}
FrontendEvent::Deleted(client) => window.delete_client(client),
FrontendEvent::State(handle, config, state) => {
window.update_client_config(handle, config);
window.update_client_state(handle, state);
}
FrontendEvent::NoSuchClient(_) => {}
FrontendEvent::Error(e) => window.show_toast(e.as_str()),
FrontendEvent::Enumerate(clients) => window.update_client_list(clients),
FrontendEvent::PortChanged(port, msg) => window.update_port(port, msg),
FrontendEvent::CaptureStatus(s) => window.set_capture(s.into()),
FrontendEvent::EmulationStatus(s) => window.set_emulation(s.into()),
FrontendEvent::AuthorizedUpdated(keys) => window.set_authorized_keys(keys),
FrontendEvent::PublicKeyFingerprint(fp) => window.set_pk_fp(&fp),
FrontendEvent::ConnectionAttempt { fingerprint } => {
window.request_authorization(&fingerprint);
FrontendEvent::Error(e) => {
window.show_toast(e.as_str());
}
FrontendEvent::DeviceConnected {
fingerprint: _,
addr,
} => {
window.show_toast(format!("device connected: {addr}").as_str());
FrontendEvent::Enumerate(clients) => {
for (handle, client, state) in clients {
if window.client_idx(handle).is_some() {
window.update_client_config(handle, client);
window.update_client_state(handle, state);
} else {
window.new_client(handle, client, state);
}
}
}
FrontendEvent::DeviceEntered {
fingerprint: _,
addr,
pos,
} => {
window.show_toast(format!("device entered: {addr} ({pos})").as_str());
FrontendEvent::PortChanged(port, msg) => {
match msg {
None => window.show_toast(format!("port changed: {port}").as_str()),
Some(msg) => window.show_toast(msg.as_str()),
}
window.imp().set_port(port);
}
FrontendEvent::IncomingDisconnected(addr) => {
window.show_toast(format!("{addr} disconnected").as_str());
FrontendEvent::CaptureStatus(s) => {
window.set_capture(s.into());
}
FrontendEvent::EmulationStatus(s) => {
window.set_emulation(s.into());
}
FrontendEvent::AuthorizedUpdated(keys) => {
window.set_authorized_keys(keys);
}
FrontendEvent::PublicKeyFingerprint(fp) => {
window.set_pk_fp(&fp);
}
FrontendEvent::IncomingConnected(..) => {}
FrontendEvent::IncomingDisconnected(..) => {}
}
}
}

View File

@@ -8,7 +8,7 @@ use glib::{clone, Object};
use gtk::{
gio,
glib::{self, closure_local},
NoSelection,
ListBox, NoSelection,
};
use lan_mouse_ipc::{
@@ -16,10 +16,7 @@ use lan_mouse_ipc::{
DEFAULT_PORT,
};
use crate::{
authorization_window::AuthorizationWindow, fingerprint_window::FingerprintWindow,
key_object::KeyObject, key_row::KeyRow,
};
use crate::{fingerprint_window::FingerprintWindow, key_object::KeyObject, key_row::KeyRow};
use super::{client_object::ClientObject, client_row::ClientRow};
@@ -31,7 +28,7 @@ glib::wrapper! {
}
impl Window {
pub(super) fn new(app: &adw::Application, conn: FrontendRequestWriter) -> Self {
pub(crate) fn new(app: &adw::Application, conn: FrontendRequestWriter) -> Self {
let window: Self = Object::builder().property("application", app).build();
window
.imp()
@@ -41,7 +38,7 @@ impl Window {
window
}
fn clients(&self) -> gio::ListStore {
pub fn clients(&self) -> gio::ListStore {
self.imp()
.clients
.borrow()
@@ -49,7 +46,7 @@ impl Window {
.expect("Could not get clients")
}
fn authorized(&self) -> gio::ListStore {
pub fn authorized(&self) -> gio::ListStore {
self.imp()
.authorized
.borrow()
@@ -65,14 +62,6 @@ impl Window {
self.authorized().item(idx).map(|o| o.downcast().unwrap())
}
fn row_by_idx(&self, idx: i32) -> Option<ClientRow> {
self.imp()
.client_list
.get()
.row_at_index(idx)
.map(|o| o.downcast().expect("expected ClientRow"))
}
fn setup_authorized(&self) {
let store = gio::ListStore::new::<KeyObject>();
self.imp().authorized.replace(Some(store));
@@ -123,57 +112,16 @@ impl Window {
.expect("Expected object of type `ClientObject`.");
let row = window.create_client_row(client_object);
row.connect_closure(
"request-hostname-change",
false,
closure_local!(
#[strong]
window,
move |row: ClientRow, hostname: String| {
log::debug!("request-hostname-change");
if let Some(client) = window.client_by_idx(row.index() as u32) {
let hostname = Some(hostname).filter(|s| !s.is_empty());
/* changed in response to FrontendEvent
* -> do not request additional update */
window.request(FrontendRequest::UpdateHostname(
client.handle(),
hostname,
));
}
}
),
);
row.connect_closure(
"request-port-change",
false,
closure_local!(
#[strong]
window,
move |row: ClientRow, port: u32| {
if let Some(client) = window.client_by_idx(row.index() as u32) {
window.request(FrontendRequest::UpdatePort(
client.handle(),
port as u16,
));
}
}
),
);
row.connect_closure(
"request-activate",
"request-update",
false,
closure_local!(
#[strong]
window,
move |row: ClientRow, active: bool| {
if let Some(client) = window.client_by_idx(row.index() as u32) {
log::debug!(
"request: {} client",
if active { "activating" } else { "deactivating" }
);
window.request(FrontendRequest::Activate(
client.handle(),
active,
));
window.request_client_activate(&client, active);
window.request_client_update(&client);
window.request_client_state(&client);
}
}
),
@@ -186,7 +134,7 @@ impl Window {
window,
move |row: ClientRow| {
if let Some(client) = window.client_by_idx(row.index() as u32) {
window.request(FrontendRequest::Delete(client.handle()));
window.request_client_delete(&client);
}
}
),
@@ -199,31 +147,9 @@ impl Window {
window,
move |row: ClientRow| {
if let Some(client) = window.client_by_idx(row.index() as u32) {
window.request(FrontendRequest::ResolveDns(
client.get_data().handle,
));
}
}
),
);
row.connect_closure(
"request-position-change",
false,
closure_local!(
#[strong]
window,
move |row: ClientRow, pos_idx: u32| {
if let Some(client) = window.client_by_idx(row.index() as u32) {
let position = match pos_idx {
0 => Position::Left,
1 => Position::Right,
2 => Position::Top,
_ => Position::Bottom,
};
window.request(FrontendRequest::UpdatePosition(
client.handle(),
position,
));
window.request_client_update(&client);
window.request_dns(&client);
window.request_client_state(&client);
}
}
),
@@ -234,14 +160,10 @@ impl Window {
);
}
fn setup_icon(&self) {
self.set_icon_name(Some("de.feschber.LanMouse"));
}
/// workaround for a bug in libadwaita that shows an ugly line beneath
/// the last element if a placeholder is set.
/// https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6308
fn update_placeholder_visibility(&self) {
pub fn update_placeholder_visibility(&self) {
let visible = self.clients().n_items() == 0;
let placeholder = self.imp().client_placeholder.get();
self.imp().client_list.set_placeholder(match visible {
@@ -250,7 +172,7 @@ impl Window {
});
}
fn update_auth_placeholder_visibility(&self) {
pub fn update_auth_placeholder_visibility(&self) {
let visible = self.authorized().n_items() == 0;
let placeholder = self.imp().authorized_placeholder.get();
self.imp().authorized_list.set_placeholder(match visible {
@@ -259,6 +181,10 @@ impl Window {
});
}
fn setup_icon(&self) {
self.set_icon_name(Some("de.feschber.LanMouse"));
}
fn create_client_row(&self, client_object: &ClientObject) -> ClientRow {
let row = ClientRow::new(client_object);
row.bind(client_object);
@@ -271,46 +197,24 @@ impl Window {
row
}
pub(super) fn new_client(
&self,
handle: ClientHandle,
client: ClientConfig,
state: ClientState,
) {
pub fn new_client(&self, handle: ClientHandle, client: ClientConfig, state: ClientState) {
let client = ClientObject::new(handle, client, state.clone());
self.clients().append(&client);
self.update_placeholder_visibility();
self.update_dns_state(handle, !state.ips.is_empty());
}
pub(super) fn update_client_list(
&self,
clients: Vec<(ClientHandle, ClientConfig, ClientState)>,
) {
for (handle, client, state) in clients {
if self.client_idx(handle).is_some() {
self.update_client_config(handle, client);
self.update_client_state(handle, state);
pub fn client_idx(&self, handle: ClientHandle) -> Option<usize> {
self.clients().iter::<ClientObject>().position(|c| {
if let Ok(c) = c {
c.handle() == handle
} else {
self.new_client(handle, client, state);
false
}
}
})
}
pub(super) fn update_port(&self, port: u16, msg: Option<String>) {
if let Some(msg) = msg {
self.show_toast(msg.as_str());
}
self.imp().set_port(port);
}
fn client_idx(&self, handle: ClientHandle) -> Option<usize> {
self.clients()
.iter::<ClientObject>()
.position(|c| c.ok().map(|c| c.handle() == handle).unwrap_or_default())
}
pub(super) fn delete_client(&self, handle: ClientHandle) {
pub fn delete_client(&self, handle: ClientHandle) {
let Some(idx) = self.client_idx(handle) else {
log::warn!("could not find client with handle {handle}");
return;
@@ -322,31 +226,46 @@ impl Window {
}
}
pub(super) fn update_client_config(&self, handle: ClientHandle, client: ClientConfig) {
let Some(row) = self.row_for_handle(handle) else {
log::warn!("could not find row for handle {handle}");
pub fn update_client_config(&self, handle: ClientHandle, client: ClientConfig) {
let Some(idx) = self.client_idx(handle) else {
log::warn!("could not find client with handle {}", handle);
return;
};
row.set_hostname(client.hostname);
row.set_port(client.port);
row.set_position(client.pos);
let client_object = self.clients().item(idx as u32).unwrap();
let client_object: &ClientObject = client_object.downcast_ref().unwrap();
let data = client_object.get_data();
/* only change if it actually has changed, otherwise
* the update signal is triggered */
if data.hostname != client.hostname {
client_object.set_hostname(client.hostname.unwrap_or("".into()));
}
if data.port != client.port as u32 {
client_object.set_port(client.port as u32);
}
if data.position != client.pos.to_string() {
client_object.set_position(client.pos.to_string());
}
}
pub(super) fn update_client_state(&self, handle: ClientHandle, state: ClientState) {
let Some(row) = self.row_for_handle(handle) else {
log::warn!("could not find row for handle {handle}");
return;
};
let Some(client_object) = self.client_object_for_handle(handle) else {
log::warn!("could not find row for handle {handle}");
pub fn update_client_state(&self, handle: ClientHandle, state: ClientState) {
let Some(idx) = self.client_idx(handle) else {
log::warn!("could not find client with handle {}", handle);
return;
};
let client_object = self.clients().item(idx as u32).unwrap();
let client_object: &ClientObject = client_object.downcast_ref().unwrap();
let data = client_object.get_data();
/* activation state */
row.set_active(state.active);
if state.active != data.active {
client_object.set_active(state.active);
log::debug!("set active to {}", state.active);
}
/* dns state */
client_object.set_resolving(state.resolving);
if state.resolving != data.resolving {
client_object.set_resolving(state.resolving);
log::debug!("resolving {}: {}", data.handle, state.resolving);
}
self.update_dns_state(handle, !state.ips.is_empty());
let ips = state
@@ -357,23 +276,22 @@ impl Window {
client_object.set_ips(ips);
}
fn client_object_for_handle(&self, handle: ClientHandle) -> Option<ClientObject> {
self.client_idx(handle)
.and_then(|i| self.client_by_idx(i as u32))
}
fn row_for_handle(&self, handle: ClientHandle) -> Option<ClientRow> {
self.client_idx(handle)
.and_then(|i| self.row_by_idx(i as i32))
}
fn update_dns_state(&self, handle: ClientHandle, resolved: bool) {
if let Some(client_row) = self.row_for_handle(handle) {
client_row.set_dns_state(resolved);
pub fn update_dns_state(&self, handle: ClientHandle, resolved: bool) {
let Some(idx) = self.client_idx(handle) else {
log::warn!("could not find client with handle {}", handle);
return;
};
let list_box: ListBox = self.imp().client_list.get();
let row = list_box.row_at_index(idx as i32).unwrap();
let client_row: ClientRow = row.downcast().expect("expected ClientRow Object");
if resolved {
client_row.imp().dns_button.set_css_classes(&["success"])
} else {
client_row.imp().dns_button.set_css_classes(&["warning"])
}
}
fn request_port_change(&self) {
pub fn request_port_change(&self) {
let port = self
.imp()
.port_entry
@@ -385,20 +303,56 @@ impl Window {
self.request(FrontendRequest::ChangePort(port));
}
fn request_capture(&self) {
pub fn request_capture(&self) {
self.request(FrontendRequest::EnableCapture);
}
fn request_emulation(&self) {
pub fn request_emulation(&self) {
self.request(FrontendRequest::EnableEmulation);
}
fn request_client_create(&self) {
pub fn request_client_state(&self, client: &ClientObject) {
self.request_client_state_for(client.handle());
}
pub fn request_client_state_for(&self, handle: ClientHandle) {
self.request(FrontendRequest::GetState(handle));
}
pub fn request_client_create(&self) {
self.request(FrontendRequest::Create);
}
fn open_fingerprint_dialog(&self, fp: Option<String>) {
let window = FingerprintWindow::new(fp);
pub fn request_dns(&self, client: &ClientObject) {
self.request(FrontendRequest::ResolveDns(client.get_data().handle));
}
pub fn request_client_update(&self, client: &ClientObject) {
let handle = client.handle();
let data = client.get_data();
let position = Position::try_from(data.position.as_str()).expect("invalid position");
let hostname = data.hostname;
let port = data.port as u16;
for event in [
FrontendRequest::UpdateHostname(handle, hostname),
FrontendRequest::UpdatePosition(handle, position),
FrontendRequest::UpdatePort(handle, port),
] {
self.request(event);
}
}
pub fn request_client_activate(&self, client: &ClientObject, active: bool) {
self.request(FrontendRequest::Activate(client.handle(), active));
}
pub fn request_client_delete(&self, client: &ClientObject) {
self.request(FrontendRequest::Delete(client.handle()));
}
pub fn open_fingerprint_dialog(&self) {
let window = FingerprintWindow::new();
window.set_transient_for(Some(self));
window.connect_closure(
"confirm-clicked",
@@ -415,15 +369,15 @@ impl Window {
window.present();
}
fn request_fingerprint_add(&self, desc: String, fp: String) {
pub fn request_fingerprint_add(&self, desc: String, fp: String) {
self.request(FrontendRequest::AuthorizeKey(desc, fp));
}
fn request_fingerprint_remove(&self, fp: String) {
pub fn request_fingerprint_remove(&self, fp: String) {
self.request(FrontendRequest::RemoveAuthorizedKey(fp));
}
fn request(&self, request: FrontendRequest) {
pub fn request(&self, request: FrontendRequest) {
let mut requester = self.imp().frontend_request_writer.borrow_mut();
let requester = requester.as_mut().unwrap();
if let Err(e) = requester.request(request) {
@@ -431,18 +385,18 @@ impl Window {
};
}
pub(super) fn show_toast(&self, msg: &str) {
pub fn show_toast(&self, msg: &str) {
let toast = adw::Toast::new(msg);
let toast_overlay = &self.imp().toast_overlay;
toast_overlay.add_toast(toast);
}
pub(super) fn set_capture(&self, active: bool) {
pub fn set_capture(&self, active: bool) {
self.imp().capture_active.replace(active);
self.update_capture_emulation_status();
}
pub(super) fn set_emulation(&self, active: bool) {
pub fn set_emulation(&self, active: bool) {
self.imp().emulation_active.replace(active);
self.update_capture_emulation_status();
}
@@ -457,7 +411,7 @@ impl Window {
.set_visible(!capture || !emulation);
}
pub(super) fn set_authorized_keys(&self, fingerprints: HashMap<String, String>) {
pub(crate) fn set_authorized_keys(&self, fingerprints: HashMap<String, String>) {
let authorized = self.authorized();
// clear list
authorized.remove_all();
@@ -469,36 +423,7 @@ impl Window {
self.update_auth_placeholder_visibility();
}
pub(super) fn set_pk_fp(&self, fingerprint: &str) {
pub(crate) fn set_pk_fp(&self, fingerprint: &str) {
self.imp().fingerprint_row.set_subtitle(fingerprint);
}
pub(super) fn request_authorization(&self, fingerprint: &str) {
if let Some(w) = self.imp().authorization_window.borrow_mut().take() {
w.close();
}
let window = AuthorizationWindow::new(fingerprint);
window.set_transient_for(Some(self));
window.connect_closure(
"confirm-clicked",
false,
closure_local!(
#[strong(rename_to = parent)]
self,
move |w: AuthorizationWindow, fp: String| {
w.close();
parent.open_fingerprint_dialog(Some(fp));
}
),
);
window.connect_closure(
"cancel-clicked",
false,
closure_local!(move |w: AuthorizationWindow| {
w.close();
}),
);
window.present();
self.imp().authorization_window.replace(Some(window));
}
}

View File

@@ -8,8 +8,6 @@ use gtk::{gdk, gio, glib, Button, CompositeTemplate, Entry, Image, Label, ListBo
use lan_mouse_ipc::{FrontendRequestWriter, DEFAULT_PORT};
use crate::authorization_window::AuthorizationWindow;
#[derive(CompositeTemplate, Default)]
#[template(resource = "/de/feschber/LanMouse/window.ui")]
pub struct Window {
@@ -51,7 +49,6 @@ pub struct Window {
pub port: Cell<u16>,
pub capture_active: Cell<bool>,
pub emulation_active: Cell<bool>,
pub authorization_window: RefCell<Option<AuthorizationWindow>>,
}
#[glib::object_subclass]
@@ -152,7 +149,7 @@ impl Window {
#[template_callback]
fn handle_add_cert_fingerprint(&self, _button: &Button) {
self.obj().open_fingerprint_dialog(None);
self.obj().open_fingerprint_dialog();
}
pub fn set_port(&self, port: u16) {

View File

@@ -1,6 +1,7 @@
use crate::{ConnectionError, FrontendEvent, FrontendRequest, IpcError};
use std::{
cmp::min,
io,
task::{ready, Poll},
time::Duration,
};
@@ -46,7 +47,7 @@ impl Stream for AsyncFrontendEventReader {
}
impl AsyncFrontendRequestWriter {
pub async fn request(&mut self, request: FrontendRequest) -> Result<(), IpcError> {
pub async fn request(&mut self, request: FrontendRequest) -> Result<(), io::Error> {
let mut json = serde_json::to_string(&request).unwrap();
log::debug!("requesting: {json}");
json.push('\n');
@@ -56,16 +57,8 @@ impl AsyncFrontendRequestWriter {
}
pub async fn connect_async(
timeout: Option<Duration>,
) -> Result<(AsyncFrontendEventReader, AsyncFrontendRequestWriter), ConnectionError> {
let stream = if let Some(duration) = timeout {
tokio::select! {
s = wait_for_service() => s?,
_ = tokio::time::sleep(duration) => return Err(ConnectionError::Timeout),
}
} else {
wait_for_service().await?
};
let stream = wait_for_service().await?;
#[cfg(unix)]
let (rx, tx): (ReadHalf<UnixStream>, WriteHalf<UnixStream>) = tokio::io::split(stream);
#[cfg(windows)]

View File

@@ -30,8 +30,6 @@ pub enum ConnectionError {
SocketPath(#[from] SocketPathError),
#[error(transparent)]
Io(#[from] io::Error),
#[error("connection timed out")]
Timeout,
}
#[derive(Debug, Error)]
@@ -59,7 +57,6 @@ pub enum IpcError {
pub const DEFAULT_PORT: u16 = 4242;
#[derive(Debug, Default, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Position {
#[default]
Left,
@@ -180,6 +177,8 @@ pub struct ClientState {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FrontendEvent {
/// client state has changed, new state must be requested via [`FrontendRequest::GetState`]
Changed(ClientHandle),
/// a client was created
Created(ClientHandle, ClientConfig, ClientState),
/// no such client
@@ -202,21 +201,10 @@ pub enum FrontendEvent {
AuthorizedUpdated(HashMap<String, String>),
/// public key fingerprint of this device
PublicKeyFingerprint(String),
/// new device connected
DeviceConnected {
addr: SocketAddr,
fingerprint: String,
},
/// incoming device entered the screen
DeviceEntered {
fingerprint: String,
addr: SocketAddr,
pos: Position,
},
/// incoming connected
IncomingConnected(String, SocketAddr, Position),
/// incoming disconnected
IncomingDisconnected(SocketAddr),
/// failed connection attempt (approval for fingerprint required)
ConnectionAttempt { fingerprint: String },
}
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
@@ -241,6 +229,8 @@ pub enum FrontendRequest {
UpdatePosition(ClientHandle, Position),
/// update fix-ips
UpdateFixIps(ClientHandle, Vec<IpAddr>),
/// request the state of the given client
GetState(ClientHandle),
/// request reenabling input capture
EnableCapture,
/// request reenabling input emulation
@@ -251,8 +241,6 @@ pub enum FrontendRequest {
AuthorizeKey(String, String),
/// remove fingerprint (fingerprint)
RemoveAuthorizedKey(String),
/// change the hook command
UpdateEnterHook(u64, Option<String>),
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]

View File

@@ -45,7 +45,7 @@ impl AsyncFrontendListener {
let (socket_path, listener) = {
let socket_path = crate::default_socket_path()?;
log::debug!("remove socket: {socket_path:?}");
log::debug!("remove socket: {:?}", socket_path);
if socket_path.exists() {
// try to connect to see if some other instance
// of lan-mouse is already running

View File

@@ -52,7 +52,7 @@ in {
};
Service = {
Type = "simple";
ExecStart = "${cfg.package}/bin/lan-mouse daemon";
ExecStart = "${cfg.package}/bin/lan-mouse --daemon";
};
Install.WantedBy = [
(lib.mkIf config.wayland.windowManager.hyprland.systemd.enable "hyprland-session.target")
@@ -65,7 +65,7 @@ in {
config = {
ProgramArguments = [
"${cfg.package}/bin/lan-mouse"
"daemon"
"--daemon"
];
KeepAlive = true;
};

View File

@@ -1,93 +0,0 @@
#!/bin/sh
set -eu
homebrew_path=""
exec_path="target/debug/bundle/osx/Lan Mouse.app/Contents/MacOS/lan-mouse"
usage() {
cat <<EOF
$0: Copy all Homebrew libraries into the macOS app bundle.
USAGE: $0 [-h] [-b homebrew_path] [exec_path]
OPTIONS:
-h, --help Show this help message and exit
-b Path to Homebrew installation (default: $homebrew_path)
exec_path Path to the main executable in the app bundle
(default: get from `brew --prefix`)
When macOS apps are linked to dynamic libraries (.dylib files),
the fully qualified path to the library is embedded in the binary.
If the libraries come from Homebrew, that means that Homebrew must be present
and the libraries must be installed in the same location on the user's machine.
This script copies all of the Homebrew libraries that an executable links to into the app bundle
and tells all the binaries in the bundle to look for them there.
EOF
}
# Gather command-line arguments
while test $# -gt 0; do
case "$1" in
-h | --help ) usage; exit 0;;
-b | --homebrew ) homebrew_path="$1"; shift 2;;
* ) exec_path="$1"; shift;;
esac
done
if [ -z "$homebrew_path" ]; then
homebrew_path="$(brew --prefix)"
fi
# Path to the .app bundle
bundle_path=$(dirname "$(dirname "$(dirname "$exec_path")")")
# Path to the Frameworks directory
fwks_path="$bundle_path/Contents/Frameworks"
mkdir -p "$fwks_path"
# Copy and fix references for a binary (executable or dylib)
#
# This function will:
# - Copy any referenced dylibs from /opt/homebrew to the Frameworks directory
# - Update the binary to reference the local copy instead
# - Add the Frameworks directory to the binary's RPATH
# - Recursively process the copied dylibs
fix_references() {
local bin="$1"
# Get all Homebrew libraries referenced by the binary
libs=$(otool -L "$bin" | awk -v homebrew="$homebrew_path" '$0 ~ homebrew {print $1}')
echo "$libs" | while IFS= read -r old_path; do
local base_name="$(basename "$old_path")"
local dest="$fwks_path/$base_name"
if [ ! -e "$dest" ]; then
echo "Copying $old_path -> $dest"
cp -f "$old_path" "$dest"
# Ensure the copied dylib is writable so that xattr -rd /path/to/Lan\ Mouse.app works.
chmod 644 "$dest"
echo "Updating $dest to have install_name of @rpath/$base_name..."
install_name_tool -id "@rpath/$base_name" "$dest"
# Recursively process this dylib
fix_references "$dest"
fi
echo "Updating $bin to reference @rpath/$base_name..."
install_name_tool -change "$old_path" "@rpath/$base_name" "$bin"
done
}
fix_references "$exec_path"
# Ensure the main executable has our Frameworks path in its RPATH
if ! otool -l "$exec_path" | grep -q "@executable_path/../Frameworks"; then
echo "Adding RPATH to $exec_path"
install_name_tool -add_rpath "@executable_path/../Frameworks" "$exec_path"
fi
# Se-sign the .app
codesign --force --deep --sign - "$bundle_path"
echo "Done!"

View File

@@ -1,42 +0,0 @@
#!/bin/sh
set -e
usage() {
cat <<EOF
$0: Make a macOS icns file from an SVG with ImageMagick and iconutil.
usage: $0 [SVG [ICNS [ICONSET]]
ARGUMENTS
SVG The SVG file to convert
Defaults to ./lan-mouse-gtk/resources/de.feschber.LanMouse.svg
ICNS The icns file to create
Defaults to ./target/icon.icns
ICONSET The iconset directory to create
Defaults to ./target/icon.iconset
This is just a temporary directory
EOF
}
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
usage
exit 0
fi
svg="${1:-./lan-mouse-gtk/resources/de.feschber.LanMouse.svg}"
icns="${2:-./target/icon.icns}"
iconset="${3:-./target/icon.iconset}"
set -u
mkdir -p "$iconset"
magick convert -background none -resize 1024x1024 "$svg" "$iconset"/icon_512x512@2x.png
magick convert -background none -resize 512x512 "$svg" "$iconset"/icon_512x512.png
magick convert -background none -resize 256x256 "$svg" "$iconset"/icon_256x256.png
magick convert -background none -resize 128x128 "$svg" "$iconset"/icon_128x128.png
magick convert -background none -resize 64x64 "$svg" "$iconset"/icon_32x32@2x.png
magick convert -background none -resize 32x32 "$svg" "$iconset"/icon_32x32.png
magick convert -background none -resize 16x16 "$svg" "$iconset"/icon_16x16.png
cp "$iconset"/icon_512x512.png "$iconset"/icon_256x256@2x.png
cp "$iconset"/icon_256x256.png "$iconset"/icon_128x128@2x.png
cp "$iconset"/icon_32x32.png "$iconset"/icon_16x16@2x.png
iconutil -c icns "$iconset" -o "$icns"

View File

@@ -6,7 +6,7 @@ After=graphical-session.target
BindsTo=graphical-session.target
[Service]
ExecStart=/usr/bin/lan-mouse daemon
ExecStart=/usr/bin/lan-mouse --daemon
Restart=on-failure
[Install]

View File

@@ -1,16 +1,12 @@
use crate::config::Config;
use clap::Args;
use futures::StreamExt;
use input_capture::{self, CaptureError, CaptureEvent, InputCapture, InputCaptureError, Position};
use input_event::{Event, KeyboardEvent};
#[derive(Args, Clone, Debug, Eq, PartialEq)]
pub struct TestCaptureArgs {}
pub async fn run(config: Config, _args: TestCaptureArgs) -> Result<(), InputCaptureError> {
pub async fn run(config: Config) -> Result<(), InputCaptureError> {
log::info!("running input capture test");
log::info!("creating input capture");
let backend = config.capture_backend().map(|b| b.into());
let backend = config.capture_backend.map(|b| b.into());
loop {
let mut input_capture = InputCapture::new(backend).await?;
log::info!("creating clients");

View File

@@ -199,13 +199,6 @@ impl ClientManager {
}
}
/// update the enter hook command of the client
pub(crate) fn set_enter_hook(&self, handle: ClientHandle, enter_hook: Option<String>) {
if let Some((c, _s)) = self.clients.borrow_mut().get_mut(handle as usize) {
c.cmd = enter_hook;
}
}
/// set resolving status of the client
pub(crate) fn set_resolving(&self, handle: ClientHandle, status: bool) {
if let Some((_, s)) = self.clients.borrow_mut().get_mut(handle as usize) {

View File

@@ -1,6 +1,4 @@
use crate::capture_test::TestCaptureArgs;
use crate::emulation_test::TestEmulationArgs;
use clap::{Parser, Subcommand, ValueEnum};
use clap::{Parser, ValueEnum};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env::{self, VarError};
@@ -12,7 +10,6 @@ use std::{collections::HashSet, io};
use thiserror::Error;
use toml;
use lan_mouse_cli::CliArgs;
use lan_mouse_ipc::{Position, DEFAULT_PORT};
use input_event::scancode::{
@@ -20,69 +17,65 @@ use input_event::scancode::{
Linux::{KeyLeftAlt, KeyLeftCtrl, KeyLeftMeta, KeyLeftShift},
};
use shadow_rs::shadow;
shadow!(build);
const CONFIG_FILE_NAME: &str = "config.toml";
const CERT_FILE_NAME: &str = "lan-mouse.pem";
fn default_path() -> Result<PathBuf, VarError> {
#[cfg(unix)]
let default_path = {
let xdg_config_home =
env::var("XDG_CONFIG_HOME").unwrap_or(format!("{}/.config", env::var("HOME")?));
format!("{xdg_config_home}/lan-mouse/")
};
#[cfg(not(unix))]
let default_path = {
let app_data =
env::var("LOCALAPPDATA").unwrap_or(format!("{}/.config", env::var("USERPROFILE")?));
format!("{app_data}\\lan-mouse\\")
};
Ok(PathBuf::from(default_path))
}
#[derive(Serialize, Deserialize, Debug)]
struct ConfigToml {
capture_backend: Option<CaptureBackend>,
emulation_backend: Option<EmulationBackend>,
port: Option<u16>,
release_bind: Option<Vec<scancode::Linux>>,
cert_path: Option<PathBuf>,
clients: Option<Vec<TomlClient>>,
authorized_fingerprints: Option<HashMap<String, String>>,
pub struct ConfigToml {
pub capture_backend: Option<CaptureBackend>,
pub emulation_backend: Option<EmulationBackend>,
pub port: Option<u16>,
pub frontend: Option<Frontend>,
pub release_bind: Option<Vec<scancode::Linux>>,
pub cert_path: Option<PathBuf>,
pub left: Option<TomlClient>,
pub right: Option<TomlClient>,
pub top: Option<TomlClient>,
pub bottom: Option<TomlClient>,
pub authorized_fingerprints: Option<HashMap<String, String>>,
}
#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
struct TomlClient {
hostname: Option<String>,
host_name: Option<String>,
ips: Option<Vec<IpAddr>>,
port: Option<u16>,
position: Option<Position>,
activate_on_startup: Option<bool>,
enter_hook: Option<String>,
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct TomlClient {
pub capture_backend: Option<CaptureBackend>,
pub hostname: Option<String>,
pub host_name: Option<String>,
pub ips: Option<Vec<IpAddr>>,
pub port: Option<u16>,
pub activate_on_startup: Option<bool>,
pub enter_hook: Option<String>,
}
impl ConfigToml {
fn new(path: &Path) -> Result<ConfigToml, ConfigError> {
pub fn new(path: &Path) -> Result<ConfigToml, ConfigError> {
let config = fs::read_to_string(path)?;
Ok(toml::from_str::<_>(&config)?)
}
}
#[derive(Parser, Debug)]
#[command(author, version=build::CLAP_LONG_VERSION, about, long_about = None)]
struct Args {
#[command(author, version=env!("GIT_DESCRIBE"), about, long_about = None)]
struct CliArgs {
/// the listen port for lan-mouse
#[arg(short, long)]
port: Option<u16>,
/// the frontend to use [cli | gtk]
#[arg(short, long)]
frontend: Option<Frontend>,
/// non-default config file location
#[arg(short, long)]
config: Option<PathBuf>,
config: Option<String>,
/// run only the service as a daemon without the frontend
#[arg(short, long)]
daemon: bool,
/// test input capture
#[arg(long)]
test_capture: bool,
/// test input emulation
#[arg(long)]
test_emulation: bool,
/// capture backend override
#[arg(long)]
@@ -95,22 +88,6 @@ struct Args {
/// path to non-default certificate location
#[arg(long)]
cert_path: Option<PathBuf>,
/// subcommands
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Subcommand, Clone, Debug, Eq, PartialEq)]
pub enum Command {
/// test input emulation
TestEmulation(TestEmulationArgs),
/// test input capture
TestCapture(TestCaptureArgs),
/// Lan Mouse commandline interface
Cli(CliArgs),
/// run in daemon mode
Daemon,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
@@ -234,16 +211,50 @@ impl Display for EmulationBackend {
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize, ValueEnum)]
pub enum Frontend {
#[serde(rename = "gtk")]
Gtk,
#[serde(rename = "cli")]
Cli,
}
impl Default for Frontend {
fn default() -> Self {
if cfg!(feature = "gtk") {
Self::Gtk
} else {
Self::Cli
}
}
}
#[derive(Debug)]
pub struct Config {
/// command line arguments
args: Args,
/// path to the certificate file used
cert_path: PathBuf,
/// path to the config file used
config_path: PathBuf,
/// the (optional) toml config and it's path
config_toml: Option<ConfigToml>,
/// the path to the configuration file used
pub path: PathBuf,
/// public key fingerprints authorized for connection
pub authorized_fingerprints: HashMap<String, String>,
/// optional input-capture backend override
pub capture_backend: Option<CaptureBackend>,
/// optional input-emulation backend override
pub emulation_backend: Option<EmulationBackend>,
/// the frontend to use
pub frontend: Frontend,
/// the port to use (initially)
pub port: u16,
/// list of clients
pub clients: Vec<(TomlClient, Position)>,
/// whether or not to run as a daemon
pub daemon: bool,
/// configured release bind
pub release_bind: Vec<scancode::Linux>,
/// test capture instead of running the app
pub test_capture: bool,
/// test emulation instead of running the app
pub test_emulation: bool,
/// path to the tls certificate to use
pub cert_path: PathBuf,
}
pub struct ConfigClient {
@@ -255,25 +266,6 @@ pub struct ConfigClient {
pub enter_hook: Option<String>,
}
impl From<TomlClient> for ConfigClient {
fn from(toml: TomlClient) -> Self {
let active = toml.activate_on_startup.unwrap_or(false);
let enter_hook = toml.enter_hook;
let hostname = toml.hostname;
let ips = HashSet::from_iter(toml.ips.into_iter().flatten());
let port = toml.port.unwrap_or(DEFAULT_PORT);
let pos = toml.position.unwrap_or_default();
Self {
ips,
hostname,
port,
pos,
active,
enter_hook,
}
}
}
#[derive(Debug, Error)]
pub enum ConfigError {
#[error(transparent)]
@@ -289,99 +281,134 @@ const DEFAULT_RELEASE_KEYS: [scancode::Linux; 4] =
impl Config {
pub fn new() -> Result<Self, ConfigError> {
let args = Args::parse();
let args = CliArgs::parse();
const CONFIG_FILE_NAME: &str = "config.toml";
const CERT_FILE_NAME: &str = "lan-mouse.pem";
#[cfg(unix)]
let config_path = {
let xdg_config_home =
env::var("XDG_CONFIG_HOME").unwrap_or(format!("{}/.config", env::var("HOME")?));
format!("{xdg_config_home}/lan-mouse/")
};
#[cfg(not(unix))]
let config_path = {
let app_data =
env::var("LOCALAPPDATA").unwrap_or(format!("{}/.config", env::var("USERPROFILE")?));
format!("{app_data}\\lan-mouse\\")
};
let config_path = PathBuf::from(config_path);
let config_file = config_path.join(CONFIG_FILE_NAME);
// --config <file> overrules default location
let config_path = args
.config
.clone()
.unwrap_or(default_path()?.join(CONFIG_FILE_NAME));
let config_file = args.config.map(PathBuf::from).unwrap_or(config_file);
let config_toml = match ConfigToml::new(&config_path) {
let mut config_toml = match ConfigToml::new(&config_file) {
Err(e) => {
log::warn!("{config_path:?}: {e}");
log::warn!("{config_file:?}: {e}");
log::warn!("Continuing without config file ...");
None
}
Ok(c) => Some(c),
};
// --cert-path <file> overrules default location
let frontend_arg = args.frontend;
let frontend_cfg = config_toml.as_ref().and_then(|c| c.frontend);
let frontend = frontend_arg.or(frontend_cfg).unwrap_or_default();
let port = args
.port
.or(config_toml.as_ref().and_then(|c| c.port))
.unwrap_or(DEFAULT_PORT);
log::debug!("{config_toml:?}");
let release_bind = config_toml
.as_ref()
.and_then(|c| c.release_bind.clone())
.unwrap_or(Vec::from_iter(DEFAULT_RELEASE_KEYS.iter().cloned()));
let capture_backend = args
.capture_backend
.or(config_toml.as_ref().and_then(|c| c.capture_backend));
let emulation_backend = args
.emulation_backend
.or(config_toml.as_ref().and_then(|c| c.emulation_backend));
let cert_path = args
.cert_path
.clone()
.or(config_toml.as_ref().and_then(|c| c.cert_path.clone()))
.unwrap_or(default_path()?.join(CERT_FILE_NAME));
.unwrap_or(config_path.join(CERT_FILE_NAME));
let authorized_fingerprints = config_toml
.as_mut()
.and_then(|c| std::mem::take(&mut c.authorized_fingerprints))
.unwrap_or_default();
let mut clients: Vec<(TomlClient, Position)> = vec![];
if let Some(config_toml) = config_toml {
if let Some(c) = config_toml.right {
clients.push((c, Position::Right))
}
if let Some(c) = config_toml.left {
clients.push((c, Position::Left))
}
if let Some(c) = config_toml.top {
clients.push((c, Position::Top))
}
if let Some(c) = config_toml.bottom {
clients.push((c, Position::Bottom))
}
}
let daemon = args.daemon;
let test_capture = args.test_capture;
let test_emulation = args.test_emulation;
Ok(Config {
args,
path: config_path,
authorized_fingerprints,
capture_backend,
emulation_backend,
daemon,
frontend,
clients,
port,
release_bind,
test_capture,
test_emulation,
cert_path,
config_path,
config_toml,
})
}
/// the command to run
pub fn command(&self) -> Option<Command> {
self.args.command.clone()
}
pub fn config_path(&self) -> &Path {
&self.config_path
}
/// public key fingerprints authorized for connection
pub fn authorized_fingerprints(&self) -> HashMap<String, String> {
self.config_toml
.as_ref()
.and_then(|c| c.authorized_fingerprints.clone())
.unwrap_or_default()
}
/// path to certificate
pub fn cert_path(&self) -> &Path {
&self.cert_path
}
/// optional input-capture backend override
pub fn capture_backend(&self) -> Option<CaptureBackend> {
self.args
.capture_backend
.or(self.config_toml.as_ref().and_then(|c| c.capture_backend))
}
/// optional input-emulation backend override
pub fn emulation_backend(&self) -> Option<EmulationBackend> {
self.args
.emulation_backend
.or(self.config_toml.as_ref().and_then(|c| c.emulation_backend))
}
/// the port to use (initially)
pub fn port(&self) -> u16 {
self.args
.port
.or(self.config_toml.as_ref().and_then(|c| c.port))
.unwrap_or(DEFAULT_PORT)
}
/// list of configured clients
pub fn clients(&self) -> Vec<ConfigClient> {
self.config_toml
.as_ref()
.map(|c| c.clients.clone())
.unwrap_or_default()
.into_iter()
.flatten()
.map(From::<TomlClient>::from)
pub fn get_clients(&self) -> Vec<ConfigClient> {
self.clients
.iter()
.map(|(c, pos)| {
let port = c.port.unwrap_or(DEFAULT_PORT);
let ips: HashSet<IpAddr> = if let Some(ips) = c.ips.as_ref() {
HashSet::from_iter(ips.iter().cloned())
} else {
HashSet::new()
};
let hostname = match &c.hostname {
Some(h) => Some(h.clone()),
None => c.host_name.clone(),
};
let active = c.activate_on_startup.unwrap_or(false);
let enter_hook = c.enter_hook.clone();
ConfigClient {
ips,
hostname,
port,
pos: *pos,
active,
enter_hook,
}
})
.collect()
}
/// release bind for returning control to the host
pub fn release_bind(&self) -> Vec<scancode::Linux> {
self.config_toml
.as_ref()
.and_then(|c| c.release_bind.clone())
.unwrap_or(Vec::from_iter(DEFAULT_RELEASE_KEYS.iter().cloned()))
}
}

View File

@@ -1,9 +1,9 @@
use std::{collections::HashMap, net::IpAddr};
use std::net::IpAddr;
use local_channel::mpsc::{channel, Receiver, Sender};
use tokio::task::{spawn_local, JoinHandle};
use hickory_resolver::{ResolveError, TokioResolver};
use hickory_resolver::{error::ResolveError, TokioAsyncResolver};
use tokio_util::sync::CancellationToken;
use lan_mouse_ipc::ClientHandle;
@@ -26,21 +26,19 @@ pub(crate) enum DnsEvent {
}
struct DnsTask {
resolver: TokioResolver,
resolver: TokioAsyncResolver,
request_rx: Receiver<DnsRequest>,
event_tx: Sender<DnsEvent>,
cancellation_token: CancellationToken,
active_tasks: HashMap<ClientHandle, JoinHandle<()>>,
}
impl DnsResolver {
pub(crate) fn new() -> Result<Self, ResolveError> {
let resolver = TokioResolver::builder_tokio()?.build();
let resolver = TokioAsyncResolver::tokio_from_system_conf()?;
let (request_tx, request_rx) = channel();
let (event_tx, event_rx) = channel();
let cancellation_token = CancellationToken::new();
let dns_task = DnsTask {
active_tasks: Default::default(),
resolver,
request_rx,
event_tx,
@@ -83,14 +81,6 @@ impl DnsTask {
while let Some(dns_request) = self.request_rx.recv().await {
let DnsRequest { handle, hostname } = dns_request;
/* abort previous dns task */
let previous_task = self.active_tasks.remove(&handle);
if let Some(task) = previous_task {
if !task.is_finished() {
task.abort();
}
}
self.event_tx
.send(DnsEvent::Resolving(handle))
.expect("channel closed");
@@ -100,7 +90,7 @@ impl DnsTask {
let resolver = self.resolver.clone();
let cancellation_token = self.cancellation_token.clone();
let task = tokio::task::spawn_local(async move {
tokio::task::spawn_local(async move {
tokio::select! {
ips = resolver.lookup_ip(&hostname) => {
let ips = ips.map(|ips| ips.iter().collect::<Vec<_>>());
@@ -111,7 +101,6 @@ impl DnsTask {
_ = cancellation_token.cancelled() => {},
}
});
self.active_tasks.insert(handle, task);
}
}
}

View File

@@ -1,4 +1,4 @@
use crate::listen::{LanMouseListener, ListenEvent, ListenerCreationError};
use crate::listen::{LanMouseListener, ListenerCreationError};
use futures::StreamExt;
use input_emulation::{EmulationHandle, InputEmulation, InputEmulationError};
use input_event::Event;
@@ -24,15 +24,8 @@ pub(crate) struct Emulation {
}
pub(crate) enum EmulationEvent {
Connected {
addr: SocketAddr,
fingerprint: String,
},
ConnectionAttempt {
fingerprint: String,
},
/// new connection
Entered {
Connected {
/// address of the connection
addr: SocketAddr,
/// position of the connection
@@ -41,9 +34,7 @@ pub(crate) enum EmulationEvent {
fingerprint: String,
},
/// connection closed
Disconnected {
addr: SocketAddr,
},
Disconnected { addr: SocketAddr },
/// the port of the listener has changed
PortChanged(Result<u16, ListenerCreationError>),
/// emulation was disabled
@@ -128,42 +119,33 @@ impl ListenTask {
async fn run(mut self) {
let mut interval = tokio::time::interval(Duration::from_secs(5));
let mut last_response = HashMap::new();
let mut rejected_connections = HashMap::new();
loop {
select! {
e = self.listener.next() => {match e {
Some(ListenEvent::Msg { event, addr }) => {
log::trace!("{event} <-<-<-<-<- {addr}");
last_response.insert(addr, Instant::now());
match event {
ProtoEvent::Enter(pos) => {
if let Some(fingerprint) = self.listener.get_certificate_fingerprint(addr).await {
log::info!("releasing capture: {addr} entered this device");
self.event_tx.send(EmulationEvent::ReleaseNotify).expect("channel closed");
self.listener.reply(addr, ProtoEvent::Ack(0)).await;
self.event_tx.send(EmulationEvent::Entered{addr, pos: to_ipc_pos(pos), fingerprint}).expect("channel closed");
}
}
ProtoEvent::Leave(_) => {
self.emulation_proxy.remove(addr);
e = self.listener.next() => {
let (event, addr) = match e {
Some(e) => e,
None => break,
};
log::trace!("{event} <-<-<-<-<- {addr}");
last_response.insert(addr, Instant::now());
match event {
ProtoEvent::Enter(pos) => {
if let Some(fingerprint) = self.listener.get_certificate_fingerprint(addr).await {
log::info!("releasing capture: {addr} entered this device");
self.event_tx.send(EmulationEvent::ReleaseNotify).expect("channel closed");
self.listener.reply(addr, ProtoEvent::Ack(0)).await;
self.event_tx.send(EmulationEvent::Connected{addr, pos: to_ipc_pos(pos), fingerprint}).expect("channel closed");
}
ProtoEvent::Input(event) => self.emulation_proxy.consume(event, addr),
ProtoEvent::Ping => self.listener.reply(addr, ProtoEvent::Pong(self.emulation_proxy.emulation_active.get())).await,
_ => {}
}
ProtoEvent::Leave(_) => {
self.emulation_proxy.remove(addr);
self.listener.reply(addr, ProtoEvent::Ack(0)).await;
}
ProtoEvent::Input(event) => self.emulation_proxy.consume(event, addr),
ProtoEvent::Ping => self.listener.reply(addr, ProtoEvent::Pong(self.emulation_proxy.emulation_active.get())).await,
_ => {}
}
Some(ListenEvent::Accept { addr, fingerprint }) => {
self.event_tx.send(EmulationEvent::Connected { addr, fingerprint }).expect("channel closed");
}
Some(ListenEvent::Rejected { fingerprint }) => {
if rejected_connections.insert(fingerprint.clone(), Instant::now())
.is_none_or(|i| i.elapsed() >= Duration::from_secs(2)) {
self.event_tx.send(EmulationEvent::ConnectionAttempt { fingerprint }).expect("channel closed");
}
}
None => break
}}
}
event = self.emulation_proxy.event() => {
self.event_tx.send(event).expect("channel closed");
}

View File

@@ -1,5 +1,4 @@
use crate::config::Config;
use clap::Args;
use input_emulation::{InputEmulation, InputEmulationError};
use input_event::{Event, PointerEvent};
use std::f64::consts::PI;
@@ -8,20 +7,10 @@ use std::time::{Duration, Instant};
const FREQUENCY_HZ: f64 = 1.0;
const RADIUS: f64 = 100.0;
#[derive(Args, Clone, Debug, Eq, PartialEq)]
pub struct TestEmulationArgs {
#[arg(long)]
mouse: bool,
#[arg(long)]
keyboard: bool,
#[arg(long)]
scroll: bool,
}
pub async fn run(config: Config, _args: TestEmulationArgs) -> Result<(), InputEmulationError> {
pub async fn run(config: Config) -> Result<(), InputEmulationError> {
log::info!("running input emulation test");
let backend = config.emulation_backend().map(|b| b.into());
let backend = config.emulation_backend.map(|b| b.into());
let mut emulation = InputEmulation::new(backend).await?;
emulation.create(0).await;

View File

@@ -3,15 +3,15 @@ use lan_mouse_proto::{ProtoEvent, MAX_EVENT_SIZE};
use local_channel::mpsc::{channel, Receiver, Sender};
use rustls::pki_types::CertificateDer;
use std::{
collections::{HashMap, VecDeque},
collections::HashMap,
net::SocketAddr,
rc::Rc,
sync::{Arc, Mutex, RwLock},
sync::{Arc, RwLock},
time::Duration,
};
use thiserror::Error;
use tokio::{
sync::Mutex as AsyncMutex,
sync::Mutex,
task::{spawn_local, JoinHandle},
};
use webrtc_dtls::{
@@ -34,25 +34,11 @@ pub enum ListenerCreationError {
type ArcConn = Arc<dyn Conn + Send + Sync>;
pub(crate) enum ListenEvent {
Msg {
event: ProtoEvent,
addr: SocketAddr,
},
Accept {
addr: SocketAddr,
fingerprint: String,
},
Rejected {
fingerprint: String,
},
}
pub(crate) struct LanMouseListener {
listen_rx: Receiver<ListenEvent>,
listen_tx: Sender<ListenEvent>,
listen_rx: Receiver<(ProtoEvent, SocketAddr)>,
listen_tx: Sender<(ProtoEvent, SocketAddr)>,
listen_task: JoinHandle<()>,
conns: Rc<AsyncMutex<Vec<(SocketAddr, ArcConn)>>>,
conns: Rc<Mutex<Vec<(SocketAddr, ArcConn)>>>,
request_port_change: Sender<u16>,
port_changed: Receiver<Result<u16, ListenerCreationError>>,
}
@@ -72,35 +58,26 @@ impl LanMouseListener {
let (listen_tx, listen_rx) = channel();
let (request_port_change, mut request_port_change_rx) = channel();
let (port_changed_tx, port_changed) = channel();
let connection_attempts: Arc<Mutex<VecDeque<String>>> = Default::default();
let authorized = authorized_keys.clone();
let verify_peer_certificate: Option<VerifyPeerCertificateFn> = {
let connection_attempts = connection_attempts.clone();
Some(Arc::new(
move |certs: &[Vec<u8>], _chains: &[CertificateDer<'static>]| {
assert!(certs.len() == 1);
let fingerprints = certs
.iter()
.map(|c| crypto::generate_fingerprint(c))
.collect::<Vec<_>>();
if authorized
.read()
.expect("lock")
.contains_key(&fingerprints[0])
{
Ok(())
} else {
let fingerprint = fingerprints.into_iter().next().expect("fingerprint");
connection_attempts
.lock()
.expect("lock")
.push_back(fingerprint);
Err(webrtc_dtls::Error::ErrVerifyDataMismatch)
}
},
))
};
let verify_peer_certificate: Option<VerifyPeerCertificateFn> = Some(Arc::new(
move |certs: &[Vec<u8>], _chains: &[CertificateDer<'static>]| {
assert!(certs.len() == 1);
let fingerprints = certs
.iter()
.map(|c| crypto::generate_fingerprint(c))
.collect::<Vec<_>>();
if authorized
.read()
.expect("lock")
.contains_key(&fingerprints[0])
{
Ok(())
} else {
Err(webrtc_dtls::Error::ErrVerifyDataMismatch)
}
},
));
let cfg = Config {
certificates: vec![cert.clone()],
extended_master_secret: ExtendedMasterSecretType::Require,
@@ -112,69 +89,43 @@ impl LanMouseListener {
let listen_addr = SocketAddr::new("0.0.0.0".parse().expect("invalid ip"), port);
let mut listener = listen(listen_addr, cfg.clone()).await?;
let conns: Rc<AsyncMutex<Vec<(SocketAddr, ArcConn)>>> =
Rc::new(AsyncMutex::new(Vec::new()));
let conns: Rc<Mutex<Vec<(SocketAddr, ArcConn)>>> = Rc::new(Mutex::new(Vec::new()));
let conns_clone = conns.clone();
let listen_task: JoinHandle<()> = {
let listen_tx = listen_tx.clone();
let connection_attempts = connection_attempts.clone();
spawn_local(async move {
loop {
let sleep = tokio::time::sleep(Duration::from_secs(2));
tokio::select! {
/* workaround for https://github.com/webrtc-rs/webrtc/issues/614 */
_ = sleep => continue,
c = listener.accept() => match c {
Ok((conn, addr)) => {
log::info!("dtls client connected, ip: {addr}");
let mut conns = conns_clone.lock().await;
conns.push((addr, conn.clone()));
let dtls_conn: &DTLSConn = conn.as_any().downcast_ref().expect("dtls conn");
let certs = dtls_conn.connection_state().await.peer_certificates;
let cert = certs.first().expect("cert");
let fingerprint = crypto::generate_fingerprint(cert);
listen_tx.send(ListenEvent::Accept { addr, fingerprint }).expect("channel closed");
spawn_local(read_loop(conns_clone.clone(), addr, conn, listen_tx.clone()));
},
Err(e) => {
if let Error::Std(ref e) = e {
if let Some(e) = e.0.downcast_ref::<webrtc_dtls::Error>() {
match e {
webrtc_dtls::Error::ErrVerifyDataMismatch => {
if let Some(fingerprint) = connection_attempts.lock().expect("lock").pop_front() {
listen_tx.send(ListenEvent::Rejected { fingerprint }).expect("channel closed");
}
}
_ => log::warn!("accept: {e}"),
}
} else {
log::warn!("accept: {e:?}");
}
} else {
log::warn!("accept: {e:?}");
}
let tx = listen_tx.clone();
let listen_task: JoinHandle<()> = spawn_local(async move {
loop {
let sleep = tokio::time::sleep(Duration::from_secs(2));
tokio::select! {
/* workaround for https://github.com/webrtc-rs/webrtc/issues/614 */
_ = sleep => continue,
c = listener.accept() => match c {
Ok((conn, addr)) => {
log::info!("dtls client connected, ip: {addr}");
let mut conns = conns_clone.lock().await;
conns.push((addr, conn.clone()));
spawn_local(read_loop(conns_clone.clone(), addr, conn, tx.clone()));
},
Err(e) => log::warn!("accept: {e}"),
},
port = request_port_change_rx.recv() => {
let port = port.expect("channel closed");
let listen_addr = SocketAddr::new("0.0.0.0".parse().expect("invalid ip"), port);
match listen(listen_addr, cfg.clone()).await {
Ok(new_listener) => {
let _ = listener.close().await;
listener = new_listener;
port_changed_tx.send(Ok(port)).expect("channel closed");
}
},
port = request_port_change_rx.recv() => {
let port = port.expect("channel closed");
let listen_addr = SocketAddr::new("0.0.0.0".parse().expect("invalid ip"), port);
match listen(listen_addr, cfg.clone()).await {
Ok(new_listener) => {
let _ = listener.close().await;
listener = new_listener;
port_changed_tx.send(Ok(port)).expect("channel closed");
}
Err(e) => {
log::warn!("unable to change port: {e}");
port_changed_tx.send(Err(e.into())).expect("channel closed");
}
};
},
};
}
})
};
Err(e) => {
log::warn!("unable to change port: {e}");
port_changed_tx.send(Err(e.into())).expect("channel closed");
}
};
},
};
}
});
Ok(Self {
conns,
@@ -235,7 +186,7 @@ impl LanMouseListener {
}
impl Stream for LanMouseListener {
type Item = ListenEvent;
type Item = (ProtoEvent, SocketAddr);
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
@@ -246,25 +197,23 @@ impl Stream for LanMouseListener {
}
async fn read_loop(
conns: Rc<AsyncMutex<Vec<(SocketAddr, ArcConn)>>>,
conns: Rc<Mutex<Vec<(SocketAddr, ArcConn)>>>,
addr: SocketAddr,
conn: ArcConn,
dtls_tx: Sender<ListenEvent>,
dtls_tx: Sender<(ProtoEvent, SocketAddr)>,
) -> Result<(), Error> {
let mut b = [0u8; MAX_EVENT_SIZE];
while conn.recv(&mut b).await.is_ok() {
match b.try_into() {
Ok(event) => dtls_tx
.send(ListenEvent::Msg { event, addr })
.expect("channel closed"),
Ok(event) => dtls_tx.send((event, addr)).expect("channel closed"),
Err(e) => {
log::warn!("error receiving event: {e}");
break;
}
}
}
log::info!("dtls client disconnected {addr:?}");
log::info!("dtls client disconnected {:?}", addr);
let mut conns = conns.lock().await;
let index = conns
.iter()

View File

@@ -3,18 +3,15 @@ use input_capture::InputCaptureError;
use input_emulation::InputEmulationError;
use lan_mouse::{
capture_test,
config::{self, Command, Config, ConfigError},
config::{Config, ConfigError, Frontend},
emulation_test,
service::{Service, ServiceError},
};
use lan_mouse_cli::CliError;
#[cfg(feature = "gtk")]
use lan_mouse_gtk::GtkError;
use lan_mouse_ipc::{IpcError, IpcListenerCreationError};
use std::{
future::Future,
io,
process::{self, Child},
process::{self, Child, Command},
};
use thiserror::Error;
use tokio::task::LocalSet;
@@ -33,11 +30,6 @@ enum LanMouseError {
Capture(#[from] InputCaptureError),
#[error(transparent)]
Emulation(#[from] InputEmulationError),
#[cfg(feature = "gtk")]
#[error(transparent)]
Gtk(#[from] GtkError),
#[error(transparent)]
Cli(#[from] CliError),
}
fn main() {
@@ -52,52 +44,35 @@ fn main() {
}
fn run() -> Result<(), LanMouseError> {
let config = config::Config::new()?;
match config.command() {
Some(command) => match command {
Command::TestEmulation(args) => run_async(emulation_test::run(config, args))?,
Command::TestCapture(args) => run_async(capture_test::run(config, args))?,
Command::Cli(cli_args) => run_async(lan_mouse_cli::run(cli_args))?,
Command::Daemon => {
// if daemon is specified we run the service
match run_async(run_service(config)) {
Err(LanMouseError::Service(ServiceError::IpcListen(
IpcListenerCreationError::AlreadyRunning,
))) => log::info!("service already running!"),
r => r?,
}
}
},
None => {
// otherwise start the service as a child process and
// run a frontend
#[cfg(feature = "gtk")]
{
let mut service = start_service()?;
let res = lan_mouse_gtk::run();
#[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()?;
res?;
}
#[cfg(not(feature = "gtk"))]
{
// run daemon if gtk is diabled
match run_async(run_service(config)) {
Err(LanMouseError::Service(ServiceError::IpcListen(
IpcListenerCreationError::AlreadyRunning,
))) => log::info!("service already running!"),
r => r?,
}
}
// parse config file + cli args
let config = Config::new()?;
if config.test_capture {
run_async(capture_test::run(config))?;
} else if config.test_emulation {
run_async(emulation_test::run(config))?;
} else if config.daemon {
// if daemon is specified we run the service
match run_async(run_service(config)) {
Err(LanMouseError::Service(ServiceError::IpcListen(
IpcListenerCreationError::AlreadyRunning,
))) => log::info!("service already running!"),
r => r?,
}
} else {
// otherwise start the service as a child process and
// run a frontend
let mut service = start_service()?;
run_frontend(&config)?;
#[cfg(unix)]
{
// on unix we give the service a chance to terminate gracefully
let pid = service.id() as libc::pid_t;
unsafe {
libc::kill(pid, libc::SIGINT);
}
service.wait()?;
}
service.kill()?;
}
Ok(())
@@ -119,20 +94,33 @@ where
}
fn start_service() -> Result<Child, io::Error> {
let child = process::Command::new(std::env::current_exe()?)
let child = Command::new(std::env::current_exe()?)
.args(std::env::args().skip(1))
.arg("daemon")
.arg("--daemon")
.spawn()?;
Ok(child)
}
async fn run_service(config: Config) -> Result<(), ServiceError> {
let release_bind = config.release_bind();
let config_path = config.config_path().to_owned();
log::info!("using config: {:?}", config.path);
log::info!("Press {:?} to release the mouse", config.release_bind);
let mut service = Service::new(config).await?;
log::info!("using config: {config_path:?}");
log::info!("Press {release_bind:?} to release the mouse");
service.run().await?;
log::info!("service exited!");
Ok(())
}
fn run_frontend(config: &Config) -> Result<(), IpcError> {
match config.frontend {
#[cfg(feature = "gtk")]
Frontend::Gtk => {
lan_mouse_gtk::run();
}
#[cfg(not(feature = "gtk"))]
Frontend::Gtk => panic!("gtk frontend requested but feature not enabled!"),
Frontend::Cli => {
lan_mouse_cli::run()?;
}
};
Ok(())
}

View File

@@ -9,7 +9,7 @@ use crate::{
listen::{LanMouseListener, ListenerCreationError},
};
use futures::StreamExt;
use hickory_resolver::ResolveError;
use hickory_resolver::error::ResolveError;
use lan_mouse_ipc::{
AsyncFrontendListener, ClientConfig, ClientHandle, ClientState, FrontendEvent, FrontendRequest,
IpcError, IpcListenerCreationError, Position, Status,
@@ -80,7 +80,7 @@ struct Incoming {
impl Service {
pub async fn new(config: Config) -> Result<Self, ServiceError> {
let client_manager = ClientManager::default();
for client in config.clients() {
for client in config.get_clients() {
let config = ClientConfig {
hostname: client.hostname,
fix_ips: client.ips.into_iter().collect(),
@@ -99,28 +99,28 @@ impl Service {
}
// load certificate
let cert = crypto::load_or_generate_key_and_cert(config.cert_path())?;
let cert = crypto::load_or_generate_key_and_cert(&config.cert_path)?;
let public_key_fingerprint = crypto::certificate_fingerprint(&cert);
// create frontend communication adapter, exit if already running
let frontend_listener = AsyncFrontendListener::new().await?;
let authorized_keys = Arc::new(RwLock::new(config.authorized_fingerprints()));
let authorized_keys = Arc::new(RwLock::new(config.authorized_fingerprints.clone()));
// listener + connection
let listener =
LanMouseListener::new(config.port(), cert.clone(), authorized_keys.clone()).await?;
LanMouseListener::new(config.port, cert.clone(), authorized_keys.clone()).await?;
let conn = LanMouseConnection::new(cert.clone(), client_manager.clone());
// input capture + emulation
let capture_backend = config.capture_backend().map(|b| b.into());
let capture = Capture::new(capture_backend, conn, config.release_bind());
let emulation_backend = config.emulation_backend().map(|b| b.into());
let capture_backend = config.capture_backend.map(|b| b.into());
let capture = Capture::new(capture_backend, conn, config.release_bind.clone());
let emulation_backend = config.emulation_backend.map(|b| b.into());
let emulation = Emulation::new(emulation_backend, listener);
// create dns resolver
let resolver = DnsResolver::new()?;
let port = config.port();
let port = config.port;
let service = Self {
capture,
emulation,
@@ -142,15 +142,11 @@ impl Service {
}
pub async fn run(&mut self) -> Result<(), ServiceError> {
let active = self.client_manager.active_clients();
for handle in active.iter() {
for handle in self.client_manager.active_clients() {
// small hack: `activate_client()` checks, if the client
// is already active in client_manager and does not create a
// capture barrier in that case so we have to deactivate it first
self.client_manager.deactivate_client(*handle);
}
for handle in active {
self.client_manager.deactivate_client(handle);
self.activate_client(handle);
}
@@ -190,6 +186,7 @@ impl Service {
FrontendRequest::EnableCapture => self.capture.reenable(),
FrontendRequest::EnableEmulation => self.emulation.reenable(),
FrontendRequest::Enumerate() => self.enumerate(),
FrontendRequest::GetState(handle) => self.broadcast_client(handle),
FrontendRequest::UpdateFixIps(handle, fix_ips) => self.update_fix_ips(handle, fix_ips),
FrontendRequest::UpdateHostname(handle, host) => self.update_hostname(handle, host),
FrontendRequest::UpdatePort(handle, port) => self.update_port(handle, port),
@@ -197,9 +194,6 @@ impl Service {
FrontendRequest::ResolveDns(handle) => self.resolve(handle),
FrontendRequest::Sync => self.sync_frontend(),
FrontendRequest::RemoveAuthorizedKey(key) => self.remove_authorized_key(key),
FrontendRequest::UpdateEnterHook(handle, enter_hook) => {
self.update_enter_hook(handle, enter_hook)
}
}
}
@@ -211,10 +205,7 @@ impl Service {
fn handle_emulation_event(&mut self, event: EmulationEvent) {
match event {
EmulationEvent::ConnectionAttempt { fingerprint } => {
self.notify_frontend(FrontendEvent::ConnectionAttempt { fingerprint });
}
EmulationEvent::Entered {
EmulationEvent::Connected {
addr,
pos,
fingerprint,
@@ -222,11 +213,7 @@ impl Service {
// check if already registered
if !self.incoming_conns.contains(&addr) {
self.add_incoming(addr, pos, fingerprint.clone());
self.notify_frontend(FrontendEvent::DeviceEntered {
fingerprint,
addr,
pos,
});
self.notify_frontend(FrontendEvent::IncomingConnected(fingerprint, addr, pos));
} else {
self.update_incoming(addr, pos, fingerprint);
}
@@ -253,9 +240,6 @@ impl Service {
self.notify_frontend(FrontendEvent::EmulationStatus(self.emulation_status));
}
EmulationEvent::ReleaseNotify => self.capture.release(),
EmulationEvent::Connected { addr, fingerprint } => {
self.notify_frontend(FrontendEvent::DeviceConnected { addr, fingerprint });
}
}
}
@@ -299,7 +283,7 @@ impl Service {
handle
}
};
self.broadcast_client(handle);
self.notify_frontend(FrontendEvent::Changed(handle));
}
fn resolve(&self, handle: ClientHandle) {
@@ -357,11 +341,7 @@ impl Service {
self.remove_incoming(addr);
self.add_incoming(addr, pos, fingerprint.clone());
self.notify_frontend(FrontendEvent::IncomingDisconnected(addr));
self.notify_frontend(FrontendEvent::DeviceEntered {
fingerprint,
addr,
pos,
});
self.notify_frontend(FrontendEvent::IncomingConnected(fingerprint, addr, pos));
}
}
@@ -419,7 +399,7 @@ impl Service {
log::debug!("deactivating client {handle}");
if self.client_manager.deactivate_client(handle) {
self.capture.destroy(handle);
self.broadcast_client(handle);
self.notify_frontend(FrontendEvent::Changed(handle));
log::info!("deactivated client {handle}");
}
}
@@ -445,7 +425,7 @@ impl Service {
if self.client_manager.activate_client(handle) {
/* notify capture and frontends */
self.capture.create(handle, pos, CaptureType::Default);
self.broadcast_client(handle);
self.notify_frontend(FrontendEvent::Changed(handle));
log::info!("activated client {handle} ({pos})");
}
}
@@ -472,20 +452,19 @@ impl Service {
fn update_fix_ips(&mut self, handle: ClientHandle, fix_ips: Vec<IpAddr>) {
self.client_manager.set_fix_ips(handle, fix_ips);
self.broadcast_client(handle);
self.notify_frontend(FrontendEvent::Changed(handle));
}
fn update_hostname(&mut self, handle: ClientHandle, hostname: Option<String>) {
log::info!("hostname changed: {hostname:?}");
if self.client_manager.set_hostname(handle, hostname.clone()) {
self.resolve(handle);
}
self.broadcast_client(handle);
self.notify_frontend(FrontendEvent::Changed(handle));
}
fn update_port(&mut self, handle: ClientHandle, port: u16) {
self.client_manager.set_port(handle, port);
self.broadcast_client(handle);
self.notify_frontend(FrontendEvent::Changed(handle));
}
fn update_pos(&mut self, handle: ClientHandle, pos: Position) {
@@ -494,12 +473,7 @@ impl Service {
self.deactivate_client(handle);
self.activate_client(handle);
}
self.broadcast_client(handle);
}
fn update_enter_hook(&mut self, handle: ClientHandle, enter_hook: Option<String>) {
self.client_manager.set_enter_hook(handle, enter_hook);
self.broadcast_client(handle);
self.notify_frontend(FrontendEvent::Changed(handle));
}
fn broadcast_client(&mut self, handle: ClientHandle) {