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: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: install dependencies - name: install dependencies
run: brew install gtk4 libadwaita imagemagick run: brew install gtk4 libadwaita
- name: Release Build - name: Release Build
run: | run: |
cargo build --release cargo build --release
cp target/release/lan-mouse lan-mouse-macos-intel 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 - name: Upload build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: lan-mouse-macos-intel name: lan-mouse-macos-intel
path: target/release/bundle/osx/lan-mouse-macos-intel.zip path: lan-mouse-macos-intel
macos-aarch64-release-build: macos-aarch64-release-build:
runs-on: macos-14 runs-on: macos-14
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: install dependencies - name: install dependencies
run: brew install gtk4 libadwaita imagemagick run: brew install gtk4 libadwaita
- name: Release Build - name: Release Build
run: | run: |
cargo build --release cargo build --release
cp target/release/lan-mouse lan-mouse-macos-aarch64 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 - name: Upload build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: lan-mouse-macos-aarch64 name: lan-mouse-macos-aarch64
path: target/release/bundle/osx/lan-mouse-macos-aarch64.zip path: lan-mouse-macos-aarch64
pre-release: pre-release:
name: "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" runs-on: "ubuntu-latest"
steps: steps:
- name: Download build artifacts - name: Download build artifacts
@@ -151,6 +127,6 @@ jobs:
title: "Development Build" title: "Development Build"
files: | files: |
lan-mouse-linux/lan-mouse lan-mouse-linux/lan-mouse
lan-mouse-macos-intel/lan-mouse-macos-intel.zip lan-mouse-macos-intel/lan-mouse-macos-intel
lan-mouse-macos-aarch64/lan-mouse-macos-aarch64.zip lan-mouse-macos-aarch64/lan-mouse-macos-aarch64
lan-mouse-windows/lan-mouse-windows.zip lan-mouse-windows/lan-mouse-windows.zip

View File

@@ -98,7 +98,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: install dependencies - name: install dependencies
run: brew install gtk4 libadwaita imagemagick run: brew install gtk4 libadwaita
- name: Build - name: Build
run: cargo build --verbose run: cargo build --verbose
- name: Run tests - name: Run tests
@@ -107,30 +107,18 @@ jobs:
run: cargo fmt --check run: cargo fmt --check
- name: Clippy - name: Clippy
run: cargo clippy --all-features --all-targets -- --deny warnings 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 - name: Upload build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: Lan Mouse macOS (Intel) name: lan-mouse-macos
path: target/debug/bundle/osx/Lan Mouse macOS (Intel).zip path: target/debug/lan-mouse
build-macos-aarch64: build-macos-aarch64:
runs-on: macos-14 runs-on: macos-14
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: install dependencies - name: install dependencies
run: brew install gtk4 libadwaita imagemagick run: brew install gtk4 libadwaita
- name: Build - name: Build
run: cargo build --verbose run: cargo build --verbose
- name: Run tests - name: Run tests
@@ -139,20 +127,8 @@ jobs:
run: cargo fmt --check run: cargo fmt --check
- name: Clippy - name: Clippy
run: cargo clippy --all-features --all-targets -- --deny warnings 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 - name: Upload build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: Lan Mouse macOS (ARM) name: lan-mouse-macos-aarch64
path: target/debug/bundle/osx/Lan Mouse macOS (ARM).zip path: target/debug/lan-mouse

View File

@@ -80,60 +80,36 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: install dependencies - name: install dependencies
run: brew install gtk4 libadwaita imagemagick run: brew install gtk4 libadwaita
- name: Release Build - name: Release Build
run: | run: |
cargo build --release cargo build --release
cp target/release/lan-mouse lan-mouse-macos-intel 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 - name: Upload build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: lan-mouse-macos-intel.zip name: lan-mouse-macos-intel
path: target/release/bundle/osx/lan-mouse-macos-intel.zip path: lan-mouse-macos-intel
macos-aarch64-release-build: macos-aarch64-release-build:
runs-on: macos-14 runs-on: macos-14
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: install dependencies - name: install dependencies
run: brew install gtk4 libadwaita imagemagick run: brew install gtk4 libadwaita
- name: Release Build - name: Release Build
run: | run: |
cargo build --release cargo build --release
cp target/release/lan-mouse lan-mouse-macos-aarch64 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 - name: Upload build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: lan-mouse-macos-aarch64.zip name: lan-mouse-macos-aarch64
path: target/release/bundle/osx/lan-mouse-macos-aarch64.zip path: lan-mouse-macos-aarch64
tagged-release: tagged-release:
name: "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" runs-on: "ubuntu-latest"
steps: steps:
- name: Download build artifacts - name: Download build artifacts
@@ -145,6 +121,6 @@ jobs:
prerelease: false prerelease: false
files: | files: |
lan-mouse-linux/lan-mouse lan-mouse-linux/lan-mouse
lan-mouse-macos-intel/lan-mouse-macos-intel.zip lan-mouse-macos-intel/lan-mouse-macos-intel
lan-mouse-macos-aarch64/lan-mouse-macos-aarch64.zip lan-mouse-macos-aarch64/lan-mouse-macos-aarch64
lan-mouse-windows/lan-mouse-windows.zip lan-mouse-windows/lan-mouse-windows.zip

4
.gitignore vendored
View File

@@ -8,7 +8,3 @@ result
*.pem *.pem
*.csr *.csr
extfile.conf 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 strip = true
panic = "abort" panic = "abort"
[build-dependencies]
shadow-rs = "1.2.0"
[dependencies] [dependencies]
input-event = { path = "input-event", version = "0.3.0" } input-event = { path = "input-event", version = "0.3.0" }
input-emulation = { path = "input-emulation", version = "0.3.0", default-features = false } 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-gtk = { path = "lan-mouse-gtk", version = "0.2.0", optional = true }
lan-mouse-ipc = { path = "lan-mouse-ipc", version = "0.2.0" } lan-mouse-ipc = { path = "lan-mouse-ipc", version = "0.2.0" }
lan-mouse-proto = { path = "lan-mouse-proto", 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" toml = "0.8"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
log = "0.4.20" log = "0.4.20"
@@ -58,8 +54,8 @@ slab = "0.4.9"
thiserror = "2.0.0" thiserror = "2.0.0"
tokio-util = "0.7.11" tokio-util = "0.7.11"
local-channel = "0.1.5" local-channel = "0.1.5"
webrtc-dtls = { version = "0.12.0", features = ["pem"] } webrtc-dtls = { version = "0.10.0", features = ["pem"] }
webrtc-util = "0.11.0" webrtc-util = "0.9.0"
rustls = { version = "0.23.12", default-features = false, features = [ rustls = { version = "0.23.12", default-features = false, features = [
"std", "std",
"ring", "ring",
@@ -89,8 +85,3 @@ libei_emulation = ["input-event/libei", "input-emulation/libei"]
wlroots_emulation = ["input-emulation/wlroots"] wlroots_emulation = ["input-emulation/wlroots"]
x11_emulation = ["input-emulation/x11"] x11_emulation = ["input-emulation/x11"]
rdp_emulation = ["input-emulation/remote_desktop_portal"] 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) 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 ## Installation
@@ -81,37 +77,15 @@ paru -S lan-mouse-git
- flake: [README.md](./nix/README.md) - flake: [README.md](./nix/README.md)
</details> </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> <details>
<summary>Manual Installation</summary> <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). 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). 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 Additionally, available backends and frontends can be configured manually via
[cargo features](https://doc.rust-lang.org/cargo/reference/features.html). [cargo features](https://doc.rust-lang.org/cargo/reference/features.html).
E.g. if only support for sway is needed, the following command produces E.g. if only wayland support is needed, the following command produces
an executable with support for only the `layer-shell` capture backend an executable with just support for wayland:
and `wlroots` emulation backend:
```sh ```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) For a detailed list of available features, checkout the [Cargo.toml](./Cargo.toml)
</details> </details>
@@ -183,15 +156,7 @@ For a detailed list of available features, checkout the [Cargo.toml](./Cargo.tom
<summary>MacOS</summary> <summary>MacOS</summary>
```sh ```sh
# Install dependencies brew install libadwaita pkg-config
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
``` ```
</details> </details>
@@ -298,17 +263,19 @@ If the device still can not be entered, make sure you have UDP port `4242` (or t
<details> <details>
<summary>Command Line Interface</summary> <summary>Command Line Interface</summary>
The cli interface can be accessed by passing `cli` as a commandline argument. The cli interface can be enabled using `--frontend cli` as commandline arguments.
Use Type `help` to list the available commands.
```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.
E.g.:
```sh
$ cargo run --release -- --frontend cli
(...)
> connect <host> left|right|top|bottom
(...)
> list
(...)
> activate 0
```
</details> </details>
<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). 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 ```sh
lan-mouse daemon lan-mouse --daemon
``` ```
In order to start lan-mouse with a graphical session automatically, 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 ### Example config
> [!TIP] > [!TIP]
> key symbols in the release bind are named according > 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 > This is bound to change
```toml ```toml
@@ -354,6 +321,9 @@ release_bind = [ "KeyA", "KeyS", "KeyD", "KeyF" ]
# optional port (defaults to 4242) # optional port (defaults to 4242)
port = 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 # list of authorized tls certificate fingerprints that
# are accepted for incoming traffic # 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" "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" # define a client on the right side with host name "iridium"
[[clients]] [right]
# position (left | right | top | bottom)
position = "right"
# hostname # hostname
hostname = "iridium" hostname = "iridium"
# activate this client immediately when lan-mouse is started # activate this client immediately when lan-mouse is started
@@ -372,8 +340,7 @@ activate_on_startup = true
ips = ["192.168.178.156"] ips = ["192.168.178.156"]
# define a client on the left side with IP address 192.168.178.189 # define a client on the left side with IP address 192.168.178.189
[[clients]] [left]
position = "left"
# The hostname is optional: When no hostname is specified, # The hostname is optional: When no hostname is specified,
# at least one ip address needs to be specified. # at least one ip address needs to be specified.
hostname = "thorium" hostname = "thorium"
@@ -413,14 +380,14 @@ The following sections detail the emulation and capture backends provided by lan
### Input Emulation Support ### Input Emulation Support
| Desktop / Backend | wlroots | libei | remote-desktop portal | windows | macos | x11 | | Desktop / Backend | layer-shell | libei | windows | macos | x11 |
|---------------------------|--------------------------|--------------------------|--------------------------|--------------------------|----------------------------------------|--------------------| |---------------------------|--------------------------|--------------------------|--------------------------|----------------------------------------|-----|
| Wayland (wlroots) | :heavy_check_mark: | | | | | | | Wayland (wlroots) | :heavy_check_mark: | | | | |
| Wayland (KDE) | | :heavy_check_mark: | :heavy_check_mark: | | | | | Wayland (KDE) | :heavy_check_mark: | :heavy_check_mark: | | | |
| Wayland (Gnome) | | :heavy_check_mark: | :heavy_check_mark: | | | | | Wayland (Gnome) | | :heavy_check_mark: | | | |
| Windows | | | | :heavy_check_mark: | | | | Windows | | | :heavy_check_mark: | | |
| MacOS | | | | | :heavy_check_mark: | | | MacOS | | | | :heavy_check_mark: | |
| X11 | | | | | | :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. - `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. - `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 ### Input Capture Support
| Desktop / Backend | layer-shell | libei | windows | macos | x11 | | Desktop / Backend | wlroots | libei | remote-desktop portal | windows | macos | x11 |
|---------------------------|--------------------------|--------------------------|--------------------------|----------------------------------------|-----| |---------------------------|--------------------------|--------------------------|--------------------------|--------------------------|----------------------------------------|--------------------|
| Wayland (wlroots) | :heavy_check_mark: | | | | | | Wayland (wlroots) | :heavy_check_mark: | | | | | |
| Wayland (KDE) | :heavy_check_mark: | :heavy_check_mark: | | | | | Wayland (KDE) | | :heavy_check_mark: | :heavy_check_mark: | | | |
| Wayland (Gnome) | | :heavy_check_mark: | | | | | Wayland (Gnome) | | :heavy_check_mark: | :heavy_check_mark: | | | |
| Windows | | | :heavy_check_mark: | | | | Windows | | | | :heavy_check_mark: | | |
| MacOS | | | | :heavy_check_mark: | | | MacOS | | | | | :heavy_check_mark: | |
| X11 | | | | | WIP | | 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). - `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. - `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() { fn main() {
ShadowBuilder::builder() // commit hash
.deny_const(Default::default()) let git_describe = Command::new("git")
.build() .arg("describe")
.expect("shadow build"); .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 # example configuration
# configure release bind # capture_backend = "LayerShell"
release_bind = [ "KeyA", "KeyS", "KeyD", "KeyF" ]
# release bind
release_bind = ["KeyA", "KeyS", "KeyD", "KeyF"]
# optional port (defaults to 4242) # optional port (defaults to 4242)
port = 4242 port = 4242
# optional frontend -> defaults to gtk if available
# frontend = "gtk"
# list of authorized tls certificate fingerprints that # list of authorized tls certificate fingerprints that
# are accepted for incoming traffic # 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" "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" # define a client on the right side with host name "iridium"
[[clients]] [right]
# position (left | right | top | bottom)
position = "right"
# hostname # hostname
hostname = "iridium" hostname = "iridium"
# activate this client immediately when lan-mouse is started
activate_on_startup = true
# optional list of (known) ip addresses # optional list of (known) ip addresses
ips = ["192.168.178.156"] ips = ["192.168.178.156"]
# define a client on the left side with IP address 192.168.178.189 # define a client on the left side with IP address 192.168.178.189
[[clients]] [left]
position = "left"
# The hostname is optional: When no hostname is specified, # The hostname is optional: When no hostname is specified,
# at least one ip address needs to be specified. # at least one ip address needs to be specified.
hostname = "thorium" 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": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1752687322, "lastModified": 1728018373,
"narHash": "sha256-RKwfXA4OZROjBTQAl9WOZQFm7L8Bo93FQwSJpAiSRvo=", "narHash": "sha256-NOiTvBbRLIOe5F6RbHaAh6++BNjsb149fGZd1T4+KBg=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "6e987485eb2c77e5dcc5af4e3c70843711ef9251", "rev": "bc947f541ae55e999ffdb4013441347d83b00feb",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -29,11 +29,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1752806774, "lastModified": 1728181869,
"narHash": "sha256-4cHeoR2roN7d/3J6gT+l6o7J2hTrBIUiCwVdDNMeXzE=", "narHash": "sha256-sQXHXsjIcGEoIHkB+RO6BZdrPfB+43V1TEpyoWRI3ww=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "3c90219b3ba1c9790c45a078eae121de48a39c55", "rev": "cd46aa3906c14790ef5cbe278d9e54f2c38f95c0",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -40,21 +40,21 @@ wayland-protocols-wlr = { version = "0.3.1", features = [
"client", "client",
], optional = true } ], optional = true }
x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true } x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
ashpd = { version = "0.11.0", default-features = false, features = [ ashpd = { version = "0.10", default-features = false, features = [
"tokio", "tokio",
], optional = true } ], 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] [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 = "0.10.0"
core-foundation-sys = "0.8.6" core-foundation-sys = "0.8.6"
libc = "0.2.155" libc = "0.2.155"
keycode = "1.0.0" keycode = "0.4.0"
bitflags = "2.6.0" bitflags = "2.6.0"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
windows = { version = "0.61.2", features = [ windows = { version = "0.58.0", features = [
"Win32_System_LibraryLoader", "Win32_System_LibraryLoader",
"Win32_System_Threading", "Win32_System_Threading",
"Win32_Foundation", "Win32_Foundation",

View File

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

View File

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

View File

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

View File

@@ -1,36 +1,94 @@
use async_trait::async_trait; use async_trait::async_trait;
use core::task::{Context, Poll}; use core::task::{Context, Poll};
use event_thread::EventThread;
use futures::Stream; 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 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}; use super::{Capture, CaptureError, CaptureEvent, Position};
mod display_util; enum Request {
mod event_thread; Create(Position),
Destroy(Position),
}
pub struct WindowsInputCapture { pub struct WindowsInputCapture {
event_rx: Receiver<(Position, CaptureEvent)>, 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] #[async_trait]
impl Capture for WindowsInputCapture { impl Capture for WindowsInputCapture {
async fn create(&mut self, pos: Position) -> Result<(), CaptureError> { 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(()) Ok(())
} }
async fn destroy(&mut self, pos: Position) -> Result<(), CaptureError> { 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(()) Ok(())
} }
async fn release(&mut self) -> Result<(), CaptureError> { async fn release(&mut self) -> Result<(), CaptureError> {
self.event_thread.release_capture(); unsafe { signal_message_thread(EventType::Release) };
Ok(()) 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 { impl WindowsInputCapture {
pub(crate) fn new() -> Self { pub(crate) fn new() -> Self {
let (event_tx, event_rx) = channel(10); unsafe {
let event_thread = EventThread::new(event_tx); let (tx, rx) = channel(10);
Self { EVENT_TX.replace(tx);
event_thread, let (ready_tx, ready_rx) = mpsc::channel();
event_rx, 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", "rt",
"sync", "sync",
"signal", "signal",
"time"
] } ] }
once_cell = "1.19.0" once_cell = "1.19.0"
[target.'cfg(all(unix, not(target_os="macos")))'.dependencies] [target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
bitflags = "2.6.0"
wayland-client = { version = "0.31.1", optional = true } wayland-client = { version = "0.31.1", optional = true }
wayland-protocols = { version = "0.32.1", features = [ wayland-protocols = { version = "0.32.1", features = [
"client", "client",
@@ -40,18 +38,18 @@ wayland-protocols-misc = { version = "0.3.1", features = [
"client", "client",
], optional = true } ], optional = true }
x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true } x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
ashpd = { version = "0.11.0", default-features = false, features = [ ashpd = { version = "0.10", default-features = false, features = [
"tokio", "tokio",
], optional = true } ], 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] [target.'cfg(target_os="macos")'.dependencies]
bitflags = "2.6.0" bitflags = "2.6.0"
core-graphics = { version = "0.25.0", features = ["highsierra"] } core-graphics = { version = "0.24.0", features = ["highsierra"] }
keycode = "1.0.0" keycode = "0.4.0"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
windows = { version = "0.61.2", features = [ windows = { version = "0.58.0", features = [
"Win32_System_LibraryLoader", "Win32_System_LibraryLoader",
"Win32_System_Threading", "Win32_System_Threading",
"Win32_Foundation", "Win32_Foundation",

View File

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

View File

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

View File

@@ -2,17 +2,14 @@ use crate::error::EmulationError;
use super::{error::WlrootsEmulationCreationError, Emulation}; use super::{error::WlrootsEmulationCreationError, Emulation};
use async_trait::async_trait; use async_trait::async_trait;
use bitflags::bitflags;
use std::collections::HashMap; use std::collections::HashMap;
use std::io; use std::io;
use std::os::fd::{AsFd, OwnedFd}; 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::backend::WaylandError;
use wayland_client::WEnum; use wayland_client::WEnum;
use wayland_client::protocol::wl_keyboard::{self, WlKeyboard}; 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_client::protocol::wl_seat::WlSeat;
use wayland_protocols_wlr::virtual_pointer::v1::client::{ use wayland_protocols_wlr::virtual_pointer::v1::client::{
zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1 as VpManager, zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1 as VpManager,
@@ -31,7 +28,7 @@ use wayland_client::{
Connection, Dispatch, EventQueue, QueueHandle, Connection, Dispatch, EventQueue, QueueHandle,
}; };
use input_event::{scancode, Event, KeyboardEvent, PointerEvent}; use input_event::{Event, KeyboardEvent, PointerEvent};
use super::error::WaylandBindError; use super::error::WaylandBindError;
use super::EmulationHandle; use super::EmulationHandle;
@@ -105,11 +102,7 @@ impl State {
panic!("no keymap"); panic!("no keymap");
} }
let vinput = VirtualInput { let vinput = VirtualInput { pointer, keyboard };
pointer,
keyboard,
modifiers: Arc::new(Mutex::new(XMods::empty())),
};
self.input_for_client.insert(client, vinput); self.input_for_client.insert(client, vinput);
} }
@@ -163,13 +156,13 @@ impl Emulation for WlrootsEmulation {
async fn create(&mut self, handle: EmulationHandle) { async fn create(&mut self, handle: EmulationHandle) {
self.state.add_client(handle); self.state.add_client(handle);
if let Err(e) = self.queue.flush() { if let Err(e) = self.queue.flush() {
log::error!("{e}"); log::error!("{}", e);
} }
} }
async fn destroy(&mut self, handle: EmulationHandle) { async fn destroy(&mut self, handle: EmulationHandle) {
self.state.destroy_client(handle); self.state.destroy_client(handle);
if let Err(e) = self.queue.flush() { if let Err(e) = self.queue.flush() {
log::error!("{e}"); log::error!("{}", e);
} }
} }
async fn terminate(&mut self) { async fn terminate(&mut self) {
@@ -180,16 +173,10 @@ impl Emulation for WlrootsEmulation {
struct VirtualInput { struct VirtualInput {
pointer: Vp, pointer: Vp,
keyboard: Vk, keyboard: Vk,
modifiers: Arc<Mutex<XMods>>,
} }
impl VirtualInput { impl VirtualInput {
fn consume_event(&self, event: Event) -> Result<(), ()> { 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 { match event {
Event::Pointer(e) => { Event::Pointer(e) => {
match e { match e {
@@ -210,8 +197,7 @@ impl VirtualInput {
PointerEvent::AxisDiscrete120 { axis, value } => { PointerEvent::AxisDiscrete120 { axis, value } => {
let axis: Axis = (axis as u32).try_into()?; let axis: Axis = (axis as u32).try_into()?;
self.pointer self.pointer
.axis_discrete(now, axis, value as f64 / 8., value / 120); .axis_discrete(0, axis, value as f64 / 6., value / 120);
self.pointer.axis_source(AxisSource::Wheel);
self.pointer.frame(); self.pointer.frame();
} }
} }
@@ -220,17 +206,6 @@ impl VirtualInput {
Event::Keyboard(e) => match e { Event::Keyboard(e) => match e {
KeyboardEvent::Key { time, key, state } => { KeyboardEvent::Key { time, key, state } => {
self.keyboard.key(time, key, state as u32); 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 { KeyboardEvent::Modifiers {
depressed: mods_depressed, depressed: mods_depressed,
@@ -238,10 +213,6 @@ impl VirtualInput {
locked: mods_locked, locked: mods_locked,
group, 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 self.keyboard
.modifiers(mods_depressed, mods_latched, mods_locked, group); .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> { pub(crate) fn new() -> Result<Self, X11EmulationCreationError> {
let display = unsafe { let display = unsafe {
match xlib::XOpenDisplay(ptr::null()) { 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) Err(X11EmulationCreationError::OpenDisplay)
} }
display => Ok(display), display => Ok(display),

View File

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

View File

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

View File

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

View File

@@ -9,8 +9,6 @@ repository = "https://github.com/feschber/lan-mouse"
[dependencies] [dependencies]
futures = "0.3.30" futures = "0.3.30"
lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.2.0" } 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 = [ tokio = { version = "1.32.0", features = [
"io-util", "io-util",
"io-std", "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 futures::StreamExt;
use tokio::{
use std::{net::IpAddr, time::Duration}; io::{AsyncBufReadExt, BufReader},
use thiserror::Error; task::LocalSet,
use lan_mouse_ipc::{
connect_async, ClientHandle, ConnectionError, FrontendEvent, FrontendRequest, IpcError,
Position,
}; };
#[derive(Debug, Error)] use std::io::{self, Write};
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),
}
#[derive(Parser, Clone, Debug, PartialEq, Eq)] use self::command::{Command, CommandType};
#[command(name = "lan-mouse-cli", about = "LanMouse CLI interface")]
pub struct CliArgs {
#[command(subcommand)]
command: CliSubcommand,
}
#[derive(Args, Clone, Debug, PartialEq, Eq)] use lan_mouse_ipc::{
struct Client { AsyncFrontendEventReader, AsyncFrontendRequestWriter, ClientConfig, ClientHandle, ClientState,
#[arg(long)] FrontendEvent, FrontendRequest, IpcError, DEFAULT_PORT,
hostname: Option<String>, };
#[arg(long)]
port: Option<u16>,
#[arg(long)]
ips: Option<Vec<IpAddr>>,
#[arg(long)]
enter_hook: Option<String>,
}
#[derive(Clone, Subcommand, Debug, PartialEq, Eq)] mod command;
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 },
}
pub async fn run(args: CliArgs) -> Result<(), CliError> { pub fn run() -> Result<(), IpcError> {
execute(args.command).await?; 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(()) Ok(())
} }
async fn execute(cmd: CliSubcommand) -> Result<(), CliError> { struct Cli {
let (mut rx, mut tx) = connect_async(Some(Duration::from_millis(500))).await?; clients: Vec<(ClientHandle, ClientConfig, ClientState)>,
match cmd { changed: Option<ClientHandle>,
CliSubcommand::AddClient(Client { rx: AsyncFrontendEventReader,
hostname, tx: AsyncFrontendRequestWriter,
port, }
ips,
enter_hook, impl Cli {
}) => { fn new(rx: AsyncFrontendEventReader, tx: AsyncFrontendRequestWriter) -> Cli {
tx.request(FrontendRequest::Create).await?; Self {
while let Some(e) = rx.next().await { clients: vec![],
if let FrontendEvent::Created(handle, _, _) = e? { changed: None,
if let Some(hostname) = hostname { rx,
tx.request(FrontendRequest::UpdateHostname(handle, Some(hostname))) tx,
.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?
} }
} }
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(()) 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" hostname = "0.4.0"
log = "0.4.20" log = "0.4.20"
lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.2.0" } lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.2.0" }
thiserror = "2.0.0"
[build-dependencies] [build-dependencies]
glib-build-tools = { version = "0.20.0" } 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 --> <!-- enabled -->
<child type="prefix"> <child type="prefix">
<object class="GtkSwitch" id="enable_switch"> <object class="GtkSwitch" id="enable_switch">
<signal name="state_set" handler="handle_client_set_state" swapped="true"/>
<property name="valign">center</property> <property name="valign">center</property>
<property name="halign">end</property> <property name="halign">end</property>
<property name="tooltip-text" translatable="yes">enable</property> <property name="tooltip-text" translatable="yes">enable</property>

View File

@@ -2,7 +2,6 @@
<gresources> <gresources>
<gresource prefix="/de/feschber/LanMouse"> <gresource prefix="/de/feschber/LanMouse">
<file compressed="true" preprocess="xml-stripblanks">window.ui</file> <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">fingerprint_window.ui</file>
<file compressed="true" preprocess="xml-stripblanks">client_row.ui</file> <file compressed="true" preprocess="xml-stripblanks">client_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">key_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)] #[properties(wrapper_type = super::ClientObject)]
pub struct ClientObject { pub struct ClientObject {
#[property(name = "handle", get, set, type = ClientHandle, member = handle)] #[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 = "port", get, set, type = u32, member = port, maximum = u16::MAX as u32)]
#[property(name = "active", get, set, type = bool, member = active)] #[property(name = "active", get, set, type = bool, member = active)]
#[property(name = "position", get, set, type = String, member = position)] #[property(name = "position", get, set, type = String, member = position)]

View File

@@ -4,7 +4,7 @@ use adw::prelude::*;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use gtk::glib::{self, Object}; use gtk::glib::{self, Object};
use lan_mouse_ipc::{Position, DEFAULT_PORT}; use lan_mouse_ipc::DEFAULT_PORT;
use super::ClientObject; use super::ClientObject;
@@ -15,32 +15,25 @@ glib::wrapper! {
} }
impl ClientRow { impl ClientRow {
pub fn new(client_object: &ClientObject) -> Self { pub fn new(_client_object: &ClientObject) -> Self {
let client_row: Self = Object::builder().build(); Object::builder().build()
client_row
.imp()
.client_object
.borrow_mut()
.replace(client_object.clone());
client_row
} }
pub fn bind(&self, client_object: &ClientObject) { pub fn bind(&self, client_object: &ClientObject) {
let mut bindings = self.imp().bindings.borrow_mut(); let mut bindings = self.imp().bindings.borrow_mut();
// bind client active to switch state
let active_binding = client_object let active_binding = client_object
.bind_property("active", &self.imp().enable_switch.get(), "state") .bind_property("active", &self.imp().enable_switch.get(), "state")
.bidirectional()
.sync_create() .sync_create()
.build(); .build();
// bind client active to switch position
let switch_position_binding = client_object let switch_position_binding = client_object
.bind_property("active", &self.imp().enable_switch.get(), "active") .bind_property("active", &self.imp().enable_switch.get(), "active")
.bidirectional()
.sync_create() .sync_create()
.build(); .build();
// bind hostname to hostname edit field
let hostname_binding = client_object let hostname_binding = client_object
.bind_property("hostname", &self.imp().hostname.get(), "text") .bind_property("hostname", &self.imp().hostname.get(), "text")
.transform_to(|_, v: Option<String>| { .transform_to(|_, v: Option<String>| {
@@ -50,48 +43,72 @@ impl ClientRow {
Some("".to_string()) Some("".to_string())
} }
}) })
.transform_from(|_, v: String| {
if v.as_str().trim() == "" {
Some(None)
} else {
Some(Some(v))
}
})
.bidirectional()
.sync_create() .sync_create()
.build(); .build();
// bind hostname to title
let title_binding = client_object let title_binding = client_object
.bind_property("hostname", self, "title") .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()))) .transform_to(|_, v: Option<String>| {
.sync_create() if let Some(hostname) = v {
.build(); Some(hostname)
// 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())
} else { } else {
Some(v.to_string()) Some("<span font_style=\"italic\" font_weight=\"light\" foreground=\"darkgrey\">no hostname!</span>".to_string())
} }
}) })
.sync_create() .sync_create()
.build(); .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 let subtitle_binding = client_object
.bind_property("port", self, "subtitle") .bind_property("port", self, "subtitle")
.sync_create() .sync_create()
.build(); .build();
// bind position to selected position
let position_binding = client_object let position_binding = client_object
.bind_property("position", &self.imp().position.get(), "selected") .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() { .transform_to(|_, v: String| match v.as_str() {
"right" => Some(1u32), "right" => Some(1),
"top" => Some(2u32), "top" => Some(2u32),
"bottom" => Some(3u32), "bottom" => Some(3u32),
_ => Some(0u32), _ => Some(0u32),
}) })
.bidirectional()
.sync_create() .sync_create()
.build(); .build();
// bind resolving status to spinner visibility
let resolve_binding = client_object let resolve_binding = client_object
.bind_property( .bind_property(
"resolving", "resolving",
@@ -101,7 +118,6 @@ impl ClientRow {
.sync_create() .sync_create()
.build(); .build();
// bind ips to tooltip-text
let ip_binding = client_object let ip_binding = client_object
.bind_property("ips", &self.imp().dns_button.get(), "tooltip-text") .bind_property("ips", &self.imp().dns_button.get(), "tooltip-text")
.transform_to(|_, ips: Vec<String>| { .transform_to(|_, ips: Vec<String>| {
@@ -130,24 +146,4 @@ impl ClientRow {
binding.unbind(); 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::subclass::prelude::*;
use adw::{prelude::*, ActionRow, ComboRow}; use adw::{prelude::*, ActionRow, ComboRow};
use glib::{subclass::InitializingObject, Binding}; use glib::{subclass::InitializingObject, Binding};
use gtk::glib::clone;
use gtk::glib::subclass::Signal; use gtk::glib::subclass::Signal;
use gtk::glib::{clone, SignalHandlerId}; use gtk::{glib, Button, CompositeTemplate, Switch};
use gtk::{glib, Button, CompositeTemplate, Entry, Switch};
use lan_mouse_ipc::Position;
use std::sync::OnceLock; use std::sync::OnceLock;
use crate::client_object::ClientObject;
#[derive(CompositeTemplate, Default)] #[derive(CompositeTemplate, Default)]
#[template(resource = "/de/feschber/LanMouse/client_row.ui")] #[template(resource = "/de/feschber/LanMouse/client_row.ui")]
pub struct ClientRow { pub struct ClientRow {
@@ -31,11 +28,6 @@ pub struct ClientRow {
#[template_child] #[template_child]
pub dns_loading_indicator: TemplateChild<gtk::Spinner>, pub dns_loading_indicator: TemplateChild<gtk::Spinner>,
pub bindings: RefCell<Vec<Binding>>, 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] #[glib::object_subclass]
@@ -67,61 +59,17 @@ impl ObjectImpl for ClientRow {
row.handle_client_delete(button); 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] { fn signals() -> &'static [glib::subclass::Signal] {
static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new(); static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
SIGNALS.get_or_init(|| { SIGNALS.get_or_init(|| {
vec![ vec![
Signal::builder("request-activate") Signal::builder("request-dns").build(),
Signal::builder("request-update")
.param_types([bool::static_type()]) .param_types([bool::static_type()])
.build(), .build(),
Signal::builder("request-delete").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] #[gtk::template_callbacks]
impl ClientRow { impl ClientRow {
#[template_callback] #[template_callback]
fn handle_activate_switch(&self, state: bool, _switch: &Switch) -> bool { fn handle_client_set_state(&self, state: bool, _switch: &Switch) -> bool {
self.obj().emit_by_name::<()>("request-activate", &[&state]); log::debug!("state change -> requesting update");
self.obj().emit_by_name::<()>("request-update", &[&state]);
true // dont run default handler true // dont run default handler
} }
#[template_callback] #[template_callback]
fn handle_request_dns(&self, _: &Button) { fn handle_request_dns(&self, _: Button) {
self.obj().emit_by_name::<()>("request-dns", &[]); self.obj().emit_by_name::<()>("request-dns", &[]);
} }
#[template_callback] #[template_callback]
fn handle_client_delete(&self, _button: &Button) { fn handle_client_delete(&self, _button: &Button) {
log::debug!("delete button pressed -> requesting delete");
self.obj().emit_by_name::<()>("request-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 {} impl WidgetImpl for ClientRow {}

View File

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

View File

@@ -8,7 +8,7 @@ use super::KeyObject;
glib::wrapper! { glib::wrapper! {
pub struct KeyRow(ObjectSubclass<imp::KeyRow>) 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; @implements gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget;
} }

View File

@@ -1,4 +1,3 @@
mod authorization_window;
mod client_object; mod client_object;
mod client_row; mod client_row;
mod fingerprint_window; mod fingerprint_window;
@@ -10,24 +9,18 @@ use std::{env, process, str};
use window::Window; use window::Window;
use lan_mouse_ipc::FrontendEvent; use lan_mouse_ipc::{FrontendEvent, FrontendRequest};
use adw::Application; 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 gtk::{gio, glib, prelude::ApplicationExt};
use self::client_object::ClientObject; use self::client_object::ClientObject;
use self::key_object::KeyObject; use self::key_object::KeyObject;
use thiserror::Error; pub fn run() -> glib::ExitCode {
#[derive(Error, Debug)]
pub enum GtkError {
#[error("gtk frontend exited with non zero exit code: {0}")]
NonZeroExitCode(i32),
}
pub fn run() -> Result<(), GtkError> {
log::debug!("running gtk frontend"); log::debug!("running gtk frontend");
#[cfg(windows)] #[cfg(windows)]
let ret = std::thread::Builder::new() let ret = std::thread::Builder::new()
@@ -40,10 +33,13 @@ pub fn run() -> Result<(), GtkError> {
#[cfg(not(windows))] #[cfg(not(windows))]
let ret = gtk_main(); let ret = gtk_main();
match ret { if ret == glib::ExitCode::FAILURE {
glib::ExitCode::SUCCESS => Ok(()), log::error!("frontend exited with failure");
e => Err(GtkError::NonZeroExitCode(e.value())), } else {
log::info!("frontend exited successfully");
} }
ret
} }
fn gtk_main() -> glib::ExitCode { fn gtk_main() -> glib::ExitCode {
@@ -53,11 +49,7 @@ fn gtk_main() -> glib::ExitCode {
.application_id("de.feschber.LanMouse") .application_id("de.feschber.LanMouse")
.build(); .build();
app.connect_startup(|app| { app.connect_startup(|_| load_icons());
load_icons();
setup_actions(app);
setup_menu(app);
});
app.connect_activate(build_ui); app.connect_activate(build_ui);
let args: Vec<&'static str> = vec![]; let args: Vec<&'static str> = vec![];
@@ -70,33 +62,6 @@ fn load_icons() {
icon_theme.add_resource_path("/de/feschber/LanMouse/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) { fn build_ui(app: &Application) {
log::debug!("connecting to lan-mouse-socket"); log::debug!("connecting to lan-mouse-socket");
let (mut frontend_rx, frontend_tx) = match lan_mouse_ipc::connect() { let (mut frontend_rx, frontend_tx) = match lan_mouse_ipc::connect() {
@@ -131,41 +96,54 @@ fn build_ui(app: &Application) {
loop { loop {
let notify = receiver.recv().await.unwrap_or_else(|_| process::exit(1)); let notify = receiver.recv().await.unwrap_or_else(|_| process::exit(1));
match notify { match notify {
FrontendEvent::Created(handle, client, state) => { FrontendEvent::Changed(handle) => {
window.new_client(handle, client, state) 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) => { FrontendEvent::State(handle, config, state) => {
window.update_client_config(handle, config); window.update_client_config(handle, config);
window.update_client_state(handle, state); window.update_client_state(handle, state);
} }
FrontendEvent::NoSuchClient(_) => {} FrontendEvent::NoSuchClient(_) => {}
FrontendEvent::Error(e) => window.show_toast(e.as_str()), FrontendEvent::Error(e) => {
FrontendEvent::Enumerate(clients) => window.update_client_list(clients), window.show_toast(e.as_str());
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::DeviceConnected { FrontendEvent::Enumerate(clients) => {
fingerprint: _, for (handle, client, state) in clients {
addr, if window.client_idx(handle).is_some() {
} => { window.update_client_config(handle, client);
window.show_toast(format!("device connected: {addr}").as_str()); window.update_client_state(handle, state);
} else {
window.new_client(handle, client, state);
}
}
} }
FrontendEvent::DeviceEntered { FrontendEvent::PortChanged(port, msg) => {
fingerprint: _, match msg {
addr, None => window.show_toast(format!("port changed: {port}").as_str()),
pos, Some(msg) => window.show_toast(msg.as_str()),
} => { }
window.show_toast(format!("device entered: {addr} ({pos})").as_str()); window.imp().set_port(port);
} }
FrontendEvent::IncomingDisconnected(addr) => { FrontendEvent::CaptureStatus(s) => {
window.show_toast(format!("{addr} disconnected").as_str()); 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::{ use gtk::{
gio, gio,
glib::{self, closure_local}, glib::{self, closure_local},
NoSelection, ListBox, NoSelection,
}; };
use lan_mouse_ipc::{ use lan_mouse_ipc::{
@@ -16,10 +16,7 @@ use lan_mouse_ipc::{
DEFAULT_PORT, DEFAULT_PORT,
}; };
use crate::{ use crate::{fingerprint_window::FingerprintWindow, key_object::KeyObject, key_row::KeyRow};
authorization_window::AuthorizationWindow, fingerprint_window::FingerprintWindow,
key_object::KeyObject, key_row::KeyRow,
};
use super::{client_object::ClientObject, client_row::ClientRow}; use super::{client_object::ClientObject, client_row::ClientRow};
@@ -31,7 +28,7 @@ glib::wrapper! {
} }
impl Window { 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(); let window: Self = Object::builder().property("application", app).build();
window window
.imp() .imp()
@@ -41,7 +38,7 @@ impl Window {
window window
} }
fn clients(&self) -> gio::ListStore { pub fn clients(&self) -> gio::ListStore {
self.imp() self.imp()
.clients .clients
.borrow() .borrow()
@@ -49,7 +46,7 @@ impl Window {
.expect("Could not get clients") .expect("Could not get clients")
} }
fn authorized(&self) -> gio::ListStore { pub fn authorized(&self) -> gio::ListStore {
self.imp() self.imp()
.authorized .authorized
.borrow() .borrow()
@@ -65,14 +62,6 @@ impl Window {
self.authorized().item(idx).map(|o| o.downcast().unwrap()) 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) { fn setup_authorized(&self) {
let store = gio::ListStore::new::<KeyObject>(); let store = gio::ListStore::new::<KeyObject>();
self.imp().authorized.replace(Some(store)); self.imp().authorized.replace(Some(store));
@@ -123,57 +112,16 @@ impl Window {
.expect("Expected object of type `ClientObject`."); .expect("Expected object of type `ClientObject`.");
let row = window.create_client_row(client_object); let row = window.create_client_row(client_object);
row.connect_closure( row.connect_closure(
"request-hostname-change", "request-update",
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",
false, false,
closure_local!( closure_local!(
#[strong] #[strong]
window, window,
move |row: ClientRow, active: bool| { move |row: ClientRow, active: bool| {
if let Some(client) = window.client_by_idx(row.index() as u32) { if let Some(client) = window.client_by_idx(row.index() as u32) {
log::debug!( window.request_client_activate(&client, active);
"request: {} client", window.request_client_update(&client);
if active { "activating" } else { "deactivating" } window.request_client_state(&client);
);
window.request(FrontendRequest::Activate(
client.handle(),
active,
));
} }
} }
), ),
@@ -186,7 +134,7 @@ impl Window {
window, window,
move |row: ClientRow| { move |row: ClientRow| {
if let Some(client) = window.client_by_idx(row.index() as u32) { 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, window,
move |row: ClientRow| { move |row: ClientRow| {
if let Some(client) = window.client_by_idx(row.index() as u32) { if let Some(client) = window.client_by_idx(row.index() as u32) {
window.request(FrontendRequest::ResolveDns( window.request_client_update(&client);
client.get_data().handle, window.request_dns(&client);
)); window.request_client_state(&client);
}
}
),
);
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,
));
} }
} }
), ),
@@ -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 /// workaround for a bug in libadwaita that shows an ugly line beneath
/// the last element if a placeholder is set. /// the last element if a placeholder is set.
/// https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6308 /// 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 visible = self.clients().n_items() == 0;
let placeholder = self.imp().client_placeholder.get(); let placeholder = self.imp().client_placeholder.get();
self.imp().client_list.set_placeholder(match visible { 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 visible = self.authorized().n_items() == 0;
let placeholder = self.imp().authorized_placeholder.get(); let placeholder = self.imp().authorized_placeholder.get();
self.imp().authorized_list.set_placeholder(match visible { 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 { fn create_client_row(&self, client_object: &ClientObject) -> ClientRow {
let row = ClientRow::new(client_object); let row = ClientRow::new(client_object);
row.bind(client_object); row.bind(client_object);
@@ -271,46 +197,24 @@ impl Window {
row row
} }
pub(super) fn new_client( pub fn new_client(&self, handle: ClientHandle, client: ClientConfig, state: ClientState) {
&self,
handle: ClientHandle,
client: ClientConfig,
state: ClientState,
) {
let client = ClientObject::new(handle, client, state.clone()); let client = ClientObject::new(handle, client, state.clone());
self.clients().append(&client); self.clients().append(&client);
self.update_placeholder_visibility(); self.update_placeholder_visibility();
self.update_dns_state(handle, !state.ips.is_empty()); self.update_dns_state(handle, !state.ips.is_empty());
} }
pub(super) fn update_client_list( pub fn client_idx(&self, handle: ClientHandle) -> Option<usize> {
&self, self.clients().iter::<ClientObject>().position(|c| {
clients: Vec<(ClientHandle, ClientConfig, ClientState)>, if let Ok(c) = c {
) { c.handle() == handle
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);
} else { } else {
self.new_client(handle, client, state); false
} }
} })
} }
pub(super) fn update_port(&self, port: u16, msg: Option<String>) { pub fn delete_client(&self, handle: ClientHandle) {
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) {
let Some(idx) = self.client_idx(handle) else { let Some(idx) = self.client_idx(handle) else {
log::warn!("could not find client with handle {handle}"); log::warn!("could not find client with handle {handle}");
return; return;
@@ -322,31 +226,46 @@ impl Window {
} }
} }
pub(super) fn update_client_config(&self, handle: ClientHandle, client: ClientConfig) { pub fn update_client_config(&self, handle: ClientHandle, client: ClientConfig) {
let Some(row) = self.row_for_handle(handle) else { let Some(idx) = self.client_idx(handle) else {
log::warn!("could not find row for handle {handle}"); log::warn!("could not find client with handle {}", handle);
return; return;
}; };
row.set_hostname(client.hostname); let client_object = self.clients().item(idx as u32).unwrap();
row.set_port(client.port); let client_object: &ClientObject = client_object.downcast_ref().unwrap();
row.set_position(client.pos); 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) { pub fn update_client_state(&self, handle: ClientHandle, state: ClientState) {
let Some(row) = self.row_for_handle(handle) else { let Some(idx) = self.client_idx(handle) else {
log::warn!("could not find row for handle {handle}"); log::warn!("could not find client with handle {}", handle);
return;
};
let Some(client_object) = self.client_object_for_handle(handle) else {
log::warn!("could not find row for handle {handle}");
return; 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 */ if state.active != data.active {
row.set_active(state.active); client_object.set_active(state.active);
log::debug!("set active to {}", state.active);
}
/* dns state */ if state.resolving != data.resolving {
client_object.set_resolving(state.resolving); client_object.set_resolving(state.resolving);
log::debug!("resolving {}: {}", data.handle, state.resolving);
}
self.update_dns_state(handle, !state.ips.is_empty()); self.update_dns_state(handle, !state.ips.is_empty());
let ips = state let ips = state
@@ -357,23 +276,22 @@ impl Window {
client_object.set_ips(ips); client_object.set_ips(ips);
} }
fn client_object_for_handle(&self, handle: ClientHandle) -> Option<ClientObject> { pub fn update_dns_state(&self, handle: ClientHandle, resolved: bool) {
self.client_idx(handle) let Some(idx) = self.client_idx(handle) else {
.and_then(|i| self.client_by_idx(i as u32)) log::warn!("could not find client with handle {}", handle);
} return;
};
fn row_for_handle(&self, handle: ClientHandle) -> Option<ClientRow> { let list_box: ListBox = self.imp().client_list.get();
self.client_idx(handle) let row = list_box.row_at_index(idx as i32).unwrap();
.and_then(|i| self.row_by_idx(i as i32)) let client_row: ClientRow = row.downcast().expect("expected ClientRow Object");
} if resolved {
client_row.imp().dns_button.set_css_classes(&["success"])
fn update_dns_state(&self, handle: ClientHandle, resolved: bool) { } else {
if let Some(client_row) = self.row_for_handle(handle) { client_row.imp().dns_button.set_css_classes(&["warning"])
client_row.set_dns_state(resolved);
} }
} }
fn request_port_change(&self) { pub fn request_port_change(&self) {
let port = self let port = self
.imp() .imp()
.port_entry .port_entry
@@ -385,20 +303,56 @@ impl Window {
self.request(FrontendRequest::ChangePort(port)); self.request(FrontendRequest::ChangePort(port));
} }
fn request_capture(&self) { pub fn request_capture(&self) {
self.request(FrontendRequest::EnableCapture); self.request(FrontendRequest::EnableCapture);
} }
fn request_emulation(&self) { pub fn request_emulation(&self) {
self.request(FrontendRequest::EnableEmulation); 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); self.request(FrontendRequest::Create);
} }
fn open_fingerprint_dialog(&self, fp: Option<String>) { pub fn request_dns(&self, client: &ClientObject) {
let window = FingerprintWindow::new(fp); 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.set_transient_for(Some(self));
window.connect_closure( window.connect_closure(
"confirm-clicked", "confirm-clicked",
@@ -415,15 +369,15 @@ impl Window {
window.present(); 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)); 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)); 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 mut requester = self.imp().frontend_request_writer.borrow_mut();
let requester = requester.as_mut().unwrap(); let requester = requester.as_mut().unwrap();
if let Err(e) = requester.request(request) { 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 = adw::Toast::new(msg);
let toast_overlay = &self.imp().toast_overlay; let toast_overlay = &self.imp().toast_overlay;
toast_overlay.add_toast(toast); 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.imp().capture_active.replace(active);
self.update_capture_emulation_status(); 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.imp().emulation_active.replace(active);
self.update_capture_emulation_status(); self.update_capture_emulation_status();
} }
@@ -457,7 +411,7 @@ impl Window {
.set_visible(!capture || !emulation); .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(); let authorized = self.authorized();
// clear list // clear list
authorized.remove_all(); authorized.remove_all();
@@ -469,36 +423,7 @@ impl Window {
self.update_auth_placeholder_visibility(); 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); 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 lan_mouse_ipc::{FrontendRequestWriter, DEFAULT_PORT};
use crate::authorization_window::AuthorizationWindow;
#[derive(CompositeTemplate, Default)] #[derive(CompositeTemplate, Default)]
#[template(resource = "/de/feschber/LanMouse/window.ui")] #[template(resource = "/de/feschber/LanMouse/window.ui")]
pub struct Window { pub struct Window {
@@ -51,7 +49,6 @@ pub struct Window {
pub port: Cell<u16>, pub port: Cell<u16>,
pub capture_active: Cell<bool>, pub capture_active: Cell<bool>,
pub emulation_active: Cell<bool>, pub emulation_active: Cell<bool>,
pub authorization_window: RefCell<Option<AuthorizationWindow>>,
} }
#[glib::object_subclass] #[glib::object_subclass]
@@ -152,7 +149,7 @@ impl Window {
#[template_callback] #[template_callback]
fn handle_add_cert_fingerprint(&self, _button: &Button) { 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) { pub fn set_port(&self, port: u16) {

View File

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

View File

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

View File

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

View File

@@ -52,7 +52,7 @@ in {
}; };
Service = { Service = {
Type = "simple"; Type = "simple";
ExecStart = "${cfg.package}/bin/lan-mouse daemon"; ExecStart = "${cfg.package}/bin/lan-mouse --daemon";
}; };
Install.WantedBy = [ Install.WantedBy = [
(lib.mkIf config.wayland.windowManager.hyprland.systemd.enable "hyprland-session.target") (lib.mkIf config.wayland.windowManager.hyprland.systemd.enable "hyprland-session.target")
@@ -65,7 +65,7 @@ in {
config = { config = {
ProgramArguments = [ ProgramArguments = [
"${cfg.package}/bin/lan-mouse" "${cfg.package}/bin/lan-mouse"
"daemon" "--daemon"
]; ];
KeepAlive = true; 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 BindsTo=graphical-session.target
[Service] [Service]
ExecStart=/usr/bin/lan-mouse daemon ExecStart=/usr/bin/lan-mouse --daemon
Restart=on-failure Restart=on-failure
[Install] [Install]

View File

@@ -1,16 +1,12 @@
use crate::config::Config; use crate::config::Config;
use clap::Args;
use futures::StreamExt; use futures::StreamExt;
use input_capture::{self, CaptureError, CaptureEvent, InputCapture, InputCaptureError, Position}; use input_capture::{self, CaptureError, CaptureEvent, InputCapture, InputCaptureError, Position};
use input_event::{Event, KeyboardEvent}; use input_event::{Event, KeyboardEvent};
#[derive(Args, Clone, Debug, Eq, PartialEq)] pub async fn run(config: Config) -> Result<(), InputCaptureError> {
pub struct TestCaptureArgs {}
pub async fn run(config: Config, _args: TestCaptureArgs) -> Result<(), InputCaptureError> {
log::info!("running input capture test"); log::info!("running input capture test");
log::info!("creating input capture"); log::info!("creating input capture");
let backend = config.capture_backend().map(|b| b.into()); let backend = config.capture_backend.map(|b| b.into());
loop { loop {
let mut input_capture = InputCapture::new(backend).await?; let mut input_capture = InputCapture::new(backend).await?;
log::info!("creating clients"); 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 /// set resolving status of the client
pub(crate) fn set_resolving(&self, handle: ClientHandle, status: bool) { pub(crate) fn set_resolving(&self, handle: ClientHandle, status: bool) {
if let Some((_, s)) = self.clients.borrow_mut().get_mut(handle as usize) { 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 clap::{Parser, ValueEnum};
use crate::emulation_test::TestEmulationArgs;
use clap::{Parser, Subcommand, ValueEnum};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::env::{self, VarError}; use std::env::{self, VarError};
@@ -12,7 +10,6 @@ use std::{collections::HashSet, io};
use thiserror::Error; use thiserror::Error;
use toml; use toml;
use lan_mouse_cli::CliArgs;
use lan_mouse_ipc::{Position, DEFAULT_PORT}; use lan_mouse_ipc::{Position, DEFAULT_PORT};
use input_event::scancode::{ use input_event::scancode::{
@@ -20,69 +17,65 @@ use input_event::scancode::{
Linux::{KeyLeftAlt, KeyLeftCtrl, KeyLeftMeta, KeyLeftShift}, 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)] #[derive(Serialize, Deserialize, Debug)]
struct ConfigToml { pub struct ConfigToml {
capture_backend: Option<CaptureBackend>, pub capture_backend: Option<CaptureBackend>,
emulation_backend: Option<EmulationBackend>, pub emulation_backend: Option<EmulationBackend>,
port: Option<u16>, pub port: Option<u16>,
release_bind: Option<Vec<scancode::Linux>>, pub frontend: Option<Frontend>,
cert_path: Option<PathBuf>, pub release_bind: Option<Vec<scancode::Linux>>,
clients: Option<Vec<TomlClient>>, pub cert_path: Option<PathBuf>,
authorized_fingerprints: Option<HashMap<String, String>>, 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)] #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
struct TomlClient { pub struct TomlClient {
hostname: Option<String>, pub capture_backend: Option<CaptureBackend>,
host_name: Option<String>, pub hostname: Option<String>,
ips: Option<Vec<IpAddr>>, pub host_name: Option<String>,
port: Option<u16>, pub ips: Option<Vec<IpAddr>>,
position: Option<Position>, pub port: Option<u16>,
activate_on_startup: Option<bool>, pub activate_on_startup: Option<bool>,
enter_hook: Option<String>, pub enter_hook: Option<String>,
} }
impl ConfigToml { impl ConfigToml {
fn new(path: &Path) -> Result<ConfigToml, ConfigError> { pub fn new(path: &Path) -> Result<ConfigToml, ConfigError> {
let config = fs::read_to_string(path)?; let config = fs::read_to_string(path)?;
Ok(toml::from_str::<_>(&config)?) Ok(toml::from_str::<_>(&config)?)
} }
} }
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(author, version=build::CLAP_LONG_VERSION, about, long_about = None)] #[command(author, version=env!("GIT_DESCRIBE"), about, long_about = None)]
struct Args { struct CliArgs {
/// the listen port for lan-mouse /// the listen port for lan-mouse
#[arg(short, long)] #[arg(short, long)]
port: Option<u16>, port: Option<u16>,
/// the frontend to use [cli | gtk]
#[arg(short, long)]
frontend: Option<Frontend>,
/// non-default config file location /// non-default config file location
#[arg(short, long)] #[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 /// capture backend override
#[arg(long)] #[arg(long)]
@@ -95,22 +88,6 @@ struct Args {
/// path to non-default certificate location /// path to non-default certificate location
#[arg(long)] #[arg(long)]
cert_path: Option<PathBuf>, 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)] #[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)] #[derive(Debug)]
pub struct Config { pub struct Config {
/// command line arguments /// the path to the configuration file used
args: Args, pub path: PathBuf,
/// path to the certificate file used /// public key fingerprints authorized for connection
cert_path: PathBuf, pub authorized_fingerprints: HashMap<String, String>,
/// path to the config file used /// optional input-capture backend override
config_path: PathBuf, pub capture_backend: Option<CaptureBackend>,
/// the (optional) toml config and it's path /// optional input-emulation backend override
config_toml: Option<ConfigToml>, 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 { pub struct ConfigClient {
@@ -255,25 +266,6 @@ pub struct ConfigClient {
pub enter_hook: Option<String>, 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)] #[derive(Debug, Error)]
pub enum ConfigError { pub enum ConfigError {
#[error(transparent)] #[error(transparent)]
@@ -289,99 +281,134 @@ const DEFAULT_RELEASE_KEYS: [scancode::Linux; 4] =
impl Config { impl Config {
pub fn new() -> Result<Self, ConfigError> { 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 // --config <file> overrules default location
let config_path = args let config_file = args.config.map(PathBuf::from).unwrap_or(config_file);
.config
.clone()
.unwrap_or(default_path()?.join(CONFIG_FILE_NAME));
let config_toml = match ConfigToml::new(&config_path) { let mut config_toml = match ConfigToml::new(&config_file) {
Err(e) => { Err(e) => {
log::warn!("{config_path:?}: {e}"); log::warn!("{config_file:?}: {e}");
log::warn!("Continuing without config file ..."); log::warn!("Continuing without config file ...");
None None
} }
Ok(c) => Some(c), 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 let cert_path = args
.cert_path .cert_path
.clone()
.or(config_toml.as_ref().and_then(|c| c.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 { Ok(Config {
args, path: config_path,
authorized_fingerprints,
capture_backend,
emulation_backend,
daemon,
frontend,
clients,
port,
release_bind,
test_capture,
test_emulation,
cert_path, cert_path,
config_path,
config_toml,
}) })
} }
/// the command to run pub fn get_clients(&self) -> Vec<ConfigClient> {
pub fn command(&self) -> Option<Command> { self.clients
self.args.command.clone() .iter()
} .map(|(c, pos)| {
let port = c.port.unwrap_or(DEFAULT_PORT);
pub fn config_path(&self) -> &Path { let ips: HashSet<IpAddr> = if let Some(ips) = c.ips.as_ref() {
&self.config_path HashSet::from_iter(ips.iter().cloned())
} } else {
HashSet::new()
/// public key fingerprints authorized for connection };
pub fn authorized_fingerprints(&self) -> HashMap<String, String> { let hostname = match &c.hostname {
self.config_toml Some(h) => Some(h.clone()),
.as_ref() None => c.host_name.clone(),
.and_then(|c| c.authorized_fingerprints.clone()) };
.unwrap_or_default() let active = c.activate_on_startup.unwrap_or(false);
} let enter_hook = c.enter_hook.clone();
ConfigClient {
/// path to certificate ips,
pub fn cert_path(&self) -> &Path { hostname,
&self.cert_path port,
} pos: *pos,
active,
/// optional input-capture backend override enter_hook,
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)
.collect() .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 local_channel::mpsc::{channel, Receiver, Sender};
use tokio::task::{spawn_local, JoinHandle}; use tokio::task::{spawn_local, JoinHandle};
use hickory_resolver::{ResolveError, TokioResolver}; use hickory_resolver::{error::ResolveError, TokioAsyncResolver};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use lan_mouse_ipc::ClientHandle; use lan_mouse_ipc::ClientHandle;
@@ -26,21 +26,19 @@ pub(crate) enum DnsEvent {
} }
struct DnsTask { struct DnsTask {
resolver: TokioResolver, resolver: TokioAsyncResolver,
request_rx: Receiver<DnsRequest>, request_rx: Receiver<DnsRequest>,
event_tx: Sender<DnsEvent>, event_tx: Sender<DnsEvent>,
cancellation_token: CancellationToken, cancellation_token: CancellationToken,
active_tasks: HashMap<ClientHandle, JoinHandle<()>>,
} }
impl DnsResolver { impl DnsResolver {
pub(crate) fn new() -> Result<Self, ResolveError> { 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 (request_tx, request_rx) = channel();
let (event_tx, event_rx) = channel(); let (event_tx, event_rx) = channel();
let cancellation_token = CancellationToken::new(); let cancellation_token = CancellationToken::new();
let dns_task = DnsTask { let dns_task = DnsTask {
active_tasks: Default::default(),
resolver, resolver,
request_rx, request_rx,
event_tx, event_tx,
@@ -83,14 +81,6 @@ impl DnsTask {
while let Some(dns_request) = self.request_rx.recv().await { while let Some(dns_request) = self.request_rx.recv().await {
let DnsRequest { handle, hostname } = dns_request; 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 self.event_tx
.send(DnsEvent::Resolving(handle)) .send(DnsEvent::Resolving(handle))
.expect("channel closed"); .expect("channel closed");
@@ -100,7 +90,7 @@ impl DnsTask {
let resolver = self.resolver.clone(); let resolver = self.resolver.clone();
let cancellation_token = self.cancellation_token.clone(); let cancellation_token = self.cancellation_token.clone();
let task = tokio::task::spawn_local(async move { tokio::task::spawn_local(async move {
tokio::select! { tokio::select! {
ips = resolver.lookup_ip(&hostname) => { ips = resolver.lookup_ip(&hostname) => {
let ips = ips.map(|ips| ips.iter().collect::<Vec<_>>()); let ips = ips.map(|ips| ips.iter().collect::<Vec<_>>());
@@ -111,7 +101,6 @@ impl DnsTask {
_ = cancellation_token.cancelled() => {}, _ = 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 futures::StreamExt;
use input_emulation::{EmulationHandle, InputEmulation, InputEmulationError}; use input_emulation::{EmulationHandle, InputEmulation, InputEmulationError};
use input_event::Event; use input_event::Event;
@@ -24,15 +24,8 @@ pub(crate) struct Emulation {
} }
pub(crate) enum EmulationEvent { pub(crate) enum EmulationEvent {
Connected {
addr: SocketAddr,
fingerprint: String,
},
ConnectionAttempt {
fingerprint: String,
},
/// new connection /// new connection
Entered { Connected {
/// address of the connection /// address of the connection
addr: SocketAddr, addr: SocketAddr,
/// position of the connection /// position of the connection
@@ -41,9 +34,7 @@ pub(crate) enum EmulationEvent {
fingerprint: String, fingerprint: String,
}, },
/// connection closed /// connection closed
Disconnected { Disconnected { addr: SocketAddr },
addr: SocketAddr,
},
/// the port of the listener has changed /// the port of the listener has changed
PortChanged(Result<u16, ListenerCreationError>), PortChanged(Result<u16, ListenerCreationError>),
/// emulation was disabled /// emulation was disabled
@@ -128,42 +119,33 @@ impl ListenTask {
async fn run(mut self) { async fn run(mut self) {
let mut interval = tokio::time::interval(Duration::from_secs(5)); let mut interval = tokio::time::interval(Duration::from_secs(5));
let mut last_response = HashMap::new(); let mut last_response = HashMap::new();
let mut rejected_connections = HashMap::new();
loop { loop {
select! { select! {
e = self.listener.next() => {match e { e = self.listener.next() => {
Some(ListenEvent::Msg { event, addr }) => { let (event, addr) = match e {
log::trace!("{event} <-<-<-<-<- {addr}"); Some(e) => e,
last_response.insert(addr, Instant::now()); None => break,
match event { };
ProtoEvent::Enter(pos) => { log::trace!("{event} <-<-<-<-<- {addr}");
if let Some(fingerprint) = self.listener.get_certificate_fingerprint(addr).await { last_response.insert(addr, Instant::now());
log::info!("releasing capture: {addr} entered this device"); match event {
self.event_tx.send(EmulationEvent::ReleaseNotify).expect("channel closed"); ProtoEvent::Enter(pos) => {
self.listener.reply(addr, ProtoEvent::Ack(0)).await; if let Some(fingerprint) = self.listener.get_certificate_fingerprint(addr).await {
self.event_tx.send(EmulationEvent::Entered{addr, pos: to_ipc_pos(pos), fingerprint}).expect("channel closed"); log::info!("releasing capture: {addr} entered this device");
} self.event_tx.send(EmulationEvent::ReleaseNotify).expect("channel closed");
}
ProtoEvent::Leave(_) => {
self.emulation_proxy.remove(addr);
self.listener.reply(addr, ProtoEvent::Ack(0)).await; 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() => { event = self.emulation_proxy.event() => {
self.event_tx.send(event).expect("channel closed"); self.event_tx.send(event).expect("channel closed");
} }

View File

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

View File

@@ -3,18 +3,15 @@ use input_capture::InputCaptureError;
use input_emulation::InputEmulationError; use input_emulation::InputEmulationError;
use lan_mouse::{ use lan_mouse::{
capture_test, capture_test,
config::{self, Command, Config, ConfigError}, config::{Config, ConfigError, Frontend},
emulation_test, emulation_test,
service::{Service, ServiceError}, service::{Service, ServiceError},
}; };
use lan_mouse_cli::CliError;
#[cfg(feature = "gtk")]
use lan_mouse_gtk::GtkError;
use lan_mouse_ipc::{IpcError, IpcListenerCreationError}; use lan_mouse_ipc::{IpcError, IpcListenerCreationError};
use std::{ use std::{
future::Future, future::Future,
io, io,
process::{self, Child}, process::{self, Child, Command},
}; };
use thiserror::Error; use thiserror::Error;
use tokio::task::LocalSet; use tokio::task::LocalSet;
@@ -33,11 +30,6 @@ enum LanMouseError {
Capture(#[from] InputCaptureError), Capture(#[from] InputCaptureError),
#[error(transparent)] #[error(transparent)]
Emulation(#[from] InputEmulationError), Emulation(#[from] InputEmulationError),
#[cfg(feature = "gtk")]
#[error(transparent)]
Gtk(#[from] GtkError),
#[error(transparent)]
Cli(#[from] CliError),
} }
fn main() { fn main() {
@@ -52,52 +44,35 @@ fn main() {
} }
fn run() -> Result<(), LanMouseError> { fn run() -> Result<(), LanMouseError> {
let config = config::Config::new()?; // parse config file + cli args
match config.command() { let config = Config::new()?;
Some(command) => match command { if config.test_capture {
Command::TestEmulation(args) => run_async(emulation_test::run(config, args))?, run_async(capture_test::run(config))?;
Command::TestCapture(args) => run_async(capture_test::run(config, args))?, } else if config.test_emulation {
Command::Cli(cli_args) => run_async(lan_mouse_cli::run(cli_args))?, run_async(emulation_test::run(config))?;
Command::Daemon => { } else if config.daemon {
// if daemon is specified we run the service // if daemon is specified we run the service
match run_async(run_service(config)) { match run_async(run_service(config)) {
Err(LanMouseError::Service(ServiceError::IpcListen( Err(LanMouseError::Service(ServiceError::IpcListen(
IpcListenerCreationError::AlreadyRunning, IpcListenerCreationError::AlreadyRunning,
))) => log::info!("service already running!"), ))) => log::info!("service already running!"),
r => r?, 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?,
}
}
} }
} 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(()) Ok(())
@@ -119,20 +94,33 @@ where
} }
fn start_service() -> Result<Child, io::Error> { 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)) .args(std::env::args().skip(1))
.arg("daemon") .arg("--daemon")
.spawn()?; .spawn()?;
Ok(child) Ok(child)
} }
async fn run_service(config: Config) -> Result<(), ServiceError> { async fn run_service(config: Config) -> Result<(), ServiceError> {
let release_bind = config.release_bind(); log::info!("using config: {:?}", config.path);
let config_path = config.config_path().to_owned(); log::info!("Press {:?} to release the mouse", config.release_bind);
let mut service = Service::new(config).await?; 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?; service.run().await?;
log::info!("service exited!"); log::info!("service exited!");
Ok(()) 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}, listen::{LanMouseListener, ListenerCreationError},
}; };
use futures::StreamExt; use futures::StreamExt;
use hickory_resolver::ResolveError; use hickory_resolver::error::ResolveError;
use lan_mouse_ipc::{ use lan_mouse_ipc::{
AsyncFrontendListener, ClientConfig, ClientHandle, ClientState, FrontendEvent, FrontendRequest, AsyncFrontendListener, ClientConfig, ClientHandle, ClientState, FrontendEvent, FrontendRequest,
IpcError, IpcListenerCreationError, Position, Status, IpcError, IpcListenerCreationError, Position, Status,
@@ -80,7 +80,7 @@ struct Incoming {
impl Service { impl Service {
pub async fn new(config: Config) -> Result<Self, ServiceError> { pub async fn new(config: Config) -> Result<Self, ServiceError> {
let client_manager = ClientManager::default(); let client_manager = ClientManager::default();
for client in config.clients() { for client in config.get_clients() {
let config = ClientConfig { let config = ClientConfig {
hostname: client.hostname, hostname: client.hostname,
fix_ips: client.ips.into_iter().collect(), fix_ips: client.ips.into_iter().collect(),
@@ -99,28 +99,28 @@ impl Service {
} }
// load certificate // 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); let public_key_fingerprint = crypto::certificate_fingerprint(&cert);
// create frontend communication adapter, exit if already running // create frontend communication adapter, exit if already running
let frontend_listener = AsyncFrontendListener::new().await?; 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 // listener + connection
let listener = 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()); let conn = LanMouseConnection::new(cert.clone(), client_manager.clone());
// input capture + emulation // input capture + emulation
let capture_backend = config.capture_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()); let capture = Capture::new(capture_backend, conn, config.release_bind.clone());
let emulation_backend = config.emulation_backend().map(|b| b.into()); let emulation_backend = config.emulation_backend.map(|b| b.into());
let emulation = Emulation::new(emulation_backend, listener); let emulation = Emulation::new(emulation_backend, listener);
// create dns resolver // create dns resolver
let resolver = DnsResolver::new()?; let resolver = DnsResolver::new()?;
let port = config.port(); let port = config.port;
let service = Self { let service = Self {
capture, capture,
emulation, emulation,
@@ -142,15 +142,11 @@ impl Service {
} }
pub async fn run(&mut self) -> Result<(), ServiceError> { pub async fn run(&mut self) -> Result<(), ServiceError> {
let active = self.client_manager.active_clients(); for handle in self.client_manager.active_clients() {
for handle in active.iter() {
// small hack: `activate_client()` checks, if the client // small hack: `activate_client()` checks, if the client
// is already active in client_manager and does not create a // is already active in client_manager and does not create a
// capture barrier in that case so we have to deactivate it first // capture barrier in that case so we have to deactivate it first
self.client_manager.deactivate_client(*handle); self.client_manager.deactivate_client(handle);
}
for handle in active {
self.activate_client(handle); self.activate_client(handle);
} }
@@ -190,6 +186,7 @@ impl Service {
FrontendRequest::EnableCapture => self.capture.reenable(), FrontendRequest::EnableCapture => self.capture.reenable(),
FrontendRequest::EnableEmulation => self.emulation.reenable(), FrontendRequest::EnableEmulation => self.emulation.reenable(),
FrontendRequest::Enumerate() => self.enumerate(), FrontendRequest::Enumerate() => self.enumerate(),
FrontendRequest::GetState(handle) => self.broadcast_client(handle),
FrontendRequest::UpdateFixIps(handle, fix_ips) => self.update_fix_ips(handle, fix_ips), FrontendRequest::UpdateFixIps(handle, fix_ips) => self.update_fix_ips(handle, fix_ips),
FrontendRequest::UpdateHostname(handle, host) => self.update_hostname(handle, host), FrontendRequest::UpdateHostname(handle, host) => self.update_hostname(handle, host),
FrontendRequest::UpdatePort(handle, port) => self.update_port(handle, port), FrontendRequest::UpdatePort(handle, port) => self.update_port(handle, port),
@@ -197,9 +194,6 @@ impl Service {
FrontendRequest::ResolveDns(handle) => self.resolve(handle), FrontendRequest::ResolveDns(handle) => self.resolve(handle),
FrontendRequest::Sync => self.sync_frontend(), FrontendRequest::Sync => self.sync_frontend(),
FrontendRequest::RemoveAuthorizedKey(key) => self.remove_authorized_key(key), 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) { fn handle_emulation_event(&mut self, event: EmulationEvent) {
match event { match event {
EmulationEvent::ConnectionAttempt { fingerprint } => { EmulationEvent::Connected {
self.notify_frontend(FrontendEvent::ConnectionAttempt { fingerprint });
}
EmulationEvent::Entered {
addr, addr,
pos, pos,
fingerprint, fingerprint,
@@ -222,11 +213,7 @@ impl Service {
// check if already registered // check if already registered
if !self.incoming_conns.contains(&addr) { if !self.incoming_conns.contains(&addr) {
self.add_incoming(addr, pos, fingerprint.clone()); self.add_incoming(addr, pos, fingerprint.clone());
self.notify_frontend(FrontendEvent::DeviceEntered { self.notify_frontend(FrontendEvent::IncomingConnected(fingerprint, addr, pos));
fingerprint,
addr,
pos,
});
} else { } else {
self.update_incoming(addr, pos, fingerprint); self.update_incoming(addr, pos, fingerprint);
} }
@@ -253,9 +240,6 @@ impl Service {
self.notify_frontend(FrontendEvent::EmulationStatus(self.emulation_status)); self.notify_frontend(FrontendEvent::EmulationStatus(self.emulation_status));
} }
EmulationEvent::ReleaseNotify => self.capture.release(), EmulationEvent::ReleaseNotify => self.capture.release(),
EmulationEvent::Connected { addr, fingerprint } => {
self.notify_frontend(FrontendEvent::DeviceConnected { addr, fingerprint });
}
} }
} }
@@ -299,7 +283,7 @@ impl Service {
handle handle
} }
}; };
self.broadcast_client(handle); self.notify_frontend(FrontendEvent::Changed(handle));
} }
fn resolve(&self, handle: ClientHandle) { fn resolve(&self, handle: ClientHandle) {
@@ -357,11 +341,7 @@ impl Service {
self.remove_incoming(addr); self.remove_incoming(addr);
self.add_incoming(addr, pos, fingerprint.clone()); self.add_incoming(addr, pos, fingerprint.clone());
self.notify_frontend(FrontendEvent::IncomingDisconnected(addr)); self.notify_frontend(FrontendEvent::IncomingDisconnected(addr));
self.notify_frontend(FrontendEvent::DeviceEntered { self.notify_frontend(FrontendEvent::IncomingConnected(fingerprint, addr, pos));
fingerprint,
addr,
pos,
});
} }
} }
@@ -419,7 +399,7 @@ impl Service {
log::debug!("deactivating client {handle}"); log::debug!("deactivating client {handle}");
if self.client_manager.deactivate_client(handle) { if self.client_manager.deactivate_client(handle) {
self.capture.destroy(handle); self.capture.destroy(handle);
self.broadcast_client(handle); self.notify_frontend(FrontendEvent::Changed(handle));
log::info!("deactivated client {handle}"); log::info!("deactivated client {handle}");
} }
} }
@@ -445,7 +425,7 @@ impl Service {
if self.client_manager.activate_client(handle) { if self.client_manager.activate_client(handle) {
/* notify capture and frontends */ /* notify capture and frontends */
self.capture.create(handle, pos, CaptureType::Default); self.capture.create(handle, pos, CaptureType::Default);
self.broadcast_client(handle); self.notify_frontend(FrontendEvent::Changed(handle));
log::info!("activated client {handle} ({pos})"); log::info!("activated client {handle} ({pos})");
} }
} }
@@ -472,20 +452,19 @@ impl Service {
fn update_fix_ips(&mut self, handle: ClientHandle, fix_ips: Vec<IpAddr>) { fn update_fix_ips(&mut self, handle: ClientHandle, fix_ips: Vec<IpAddr>) {
self.client_manager.set_fix_ips(handle, fix_ips); 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>) { fn update_hostname(&mut self, handle: ClientHandle, hostname: Option<String>) {
log::info!("hostname changed: {hostname:?}");
if self.client_manager.set_hostname(handle, hostname.clone()) { if self.client_manager.set_hostname(handle, hostname.clone()) {
self.resolve(handle); self.resolve(handle);
} }
self.broadcast_client(handle); self.notify_frontend(FrontendEvent::Changed(handle));
} }
fn update_port(&mut self, handle: ClientHandle, port: u16) { fn update_port(&mut self, handle: ClientHandle, port: u16) {
self.client_manager.set_port(handle, port); 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) { fn update_pos(&mut self, handle: ClientHandle, pos: Position) {
@@ -494,12 +473,7 @@ impl Service {
self.deactivate_client(handle); self.deactivate_client(handle);
self.activate_client(handle); self.activate_client(handle);
} }
self.broadcast_client(handle); self.notify_frontend(FrontendEvent::Changed(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);
} }
fn broadcast_client(&mut self, handle: ClientHandle) { fn broadcast_client(&mut self, handle: ClientHandle) {