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
39 changed files with 1578 additions and 2138 deletions

View File

@@ -84,56 +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
- 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
- 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
@@ -147,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,28 +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
- 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
@@ -137,18 +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
- 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,56 +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
- 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
- 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
@@ -141,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

293
Cargo.lock generated
View File

@@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 3
[[package]] [[package]]
name = "addr2line" name = "addr2line"
@@ -61,15 +61,6 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.18" version = "0.6.18"
@@ -450,12 +441,6 @@ dependencies = [
"piper", "piper",
] ]
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.5.0" version = "1.5.0"
@@ -491,38 +476,6 @@ dependencies = [
"system-deps", "system-deps",
] ]
[[package]]
name = "camino"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3"
dependencies = [
"serde",
]
[[package]]
name = "cargo-platform"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea"
dependencies = [
"serde",
]
[[package]]
name = "cargo_metadata"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037"
dependencies = [
"camino",
"cargo-platform",
"semver",
"serde",
"serde_json",
"thiserror 1.0.68",
]
[[package]] [[package]]
name = "cbc" name = "cbc"
version = "0.1.2" version = "0.1.2"
@@ -538,8 +491,6 @@ version = "1.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baee610e9452a8f6f0a1b6194ec09ff9e2d85dea54432acdae41aa0761c95d70" checksum = "baee610e9452a8f6f0a1b6194ec09ff9e2d85dea54432acdae41aa0761c95d70"
dependencies = [ dependencies = [
"jobserver",
"libc",
"shlex", "shlex",
] ]
@@ -648,32 +599,6 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "const_fn"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f8a2ca5ac02d09563609681103aada9e1777d54fc57a5acd7a41404f9c93b6e"
[[package]]
name = "const_format"
version = "0.2.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd"
dependencies = [
"const_format_proc_macros",
]
[[package]]
name = "const_format_proc_macros"
version = "0.2.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.10.0" version = "0.10.0"
@@ -1307,19 +1232,6 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "git2"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fda788993cc341f69012feba8bf45c0ba4f3291fcc08e214b4d5a7332d88aff"
dependencies = [
"bitflags 2.6.0",
"libc",
"libgit2-sys",
"log",
"url",
]
[[package]] [[package]]
name = "glib" name = "glib"
version = "0.20.5" version = "0.20.5"
@@ -1631,29 +1543,6 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "iana-time-zone"
version = "0.1.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core 0.52.0",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "icu_collections" name = "icu_collections"
version = "1.5.0" version = "1.5.0"
@@ -1907,12 +1796,6 @@ version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708"
[[package]]
name = "is_debug"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8ea828c9d6638a5bd3d8b14e37502b4d56cae910ccf8a5b7f51c7a0eb1d0508"
[[package]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
version = "1.70.1" version = "1.70.1"
@@ -1925,25 +1808,6 @@ version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "jobserver"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
dependencies = [
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]] [[package]]
name = "keycode" name = "keycode"
version = "0.4.0" version = "0.4.0"
@@ -1991,7 +1855,6 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
"shadow-rs",
"slab", "slab",
"thiserror 2.0.0", "thiserror 2.0.0",
"tokio", "tokio",
@@ -2005,10 +1868,8 @@ dependencies = [
name = "lan-mouse-cli" name = "lan-mouse-cli"
version = "0.2.0" version = "0.2.0"
dependencies = [ dependencies = [
"clap",
"futures", "futures",
"lan-mouse-ipc", "lan-mouse-ipc",
"thiserror 2.0.0",
"tokio", "tokio",
] ]
@@ -2091,18 +1952,6 @@ version = "0.2.161"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
[[package]]
name = "libgit2-sys"
version = "0.18.0+1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1a117465e7e1597e8febea8bb0c410f1c7fb93b1e1cddf34363f8390367ffec"
dependencies = [
"cc",
"libc",
"libz-sys",
"pkg-config",
]
[[package]] [[package]]
name = "libloading" name = "libloading"
version = "0.8.5" version = "0.8.5"
@@ -2113,18 +1962,6 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "libz-sys"
version = "1.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df9b68e50e6e0b26f672573834882eb57759f6db9b3be2ea3c35c91188bb4eaa"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "linked-hash-map" name = "linked-hash-map"
version = "0.5.6" version = "0.5.6"
@@ -2349,15 +2186,6 @@ dependencies = [
"syn 2.0.87", "syn 2.0.87",
] ]
[[package]]
name = "num_threads"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "object" name = "object"
version = "0.36.5" version = "0.36.5"
@@ -2839,12 +2667,6 @@ dependencies = [
"untrusted", "untrusted",
] ]
[[package]]
name = "rustversion"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.18" version = "1.0.18"
@@ -2882,9 +2704,6 @@ name = "semver"
version = "1.0.23" version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "serde" name = "serde"
@@ -2960,21 +2779,6 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "shadow-rs"
version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69d433b5df1e1958a668457ebe4a9c5b7bcfe844f4eb2276ac43cf273baddd54"
dependencies = [
"cargo_metadata",
"const_format",
"git2",
"is_debug",
"serde_json",
"time",
"tzdb",
]
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
@@ -3190,9 +2994,7 @@ checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [ dependencies = [
"deranged", "deranged",
"itoa", "itoa",
"libc",
"num-conv", "num-conv",
"num_threads",
"powerfmt", "powerfmt",
"serde", "serde",
"time-core", "time-core",
@@ -3365,35 +3167,6 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "tz-rs"
version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33851b15c848fad2cf4b105c6bb66eb9512b6f6c44a4b13f57c53c73c707e2b4"
dependencies = [
"const_fn",
]
[[package]]
name = "tzdb"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b580f6b365fa89f5767cdb619a55d534d04a4e14c2d7e5b9a31e94598687fb1"
dependencies = [
"iana-time-zone",
"tz-rs",
"tzdb_data",
]
[[package]]
name = "tzdb_data"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4471adcfcbd3052e8c5b5890a04a559837444b3be26b9cbbd622063171cec9d"
dependencies = [
"tz-rs",
]
[[package]] [[package]]
name = "uds_windows" name = "uds_windows"
version = "1.1.0" version = "1.1.0"
@@ -3484,12 +3257,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]] [[package]]
name = "version-compare" name = "version-compare"
version = "0.2.0" version = "0.2.0"
@@ -3508,64 +3275,6 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn 2.0.87",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]] [[package]]
name = "wayland-backend" name = "wayland-backend"
version = "0.3.7" version = "0.3.7"

View File

@@ -23,9 +23,6 @@ lto = "fat"
strip = true strip = true
panic = "abort" panic = "abort"
[build-dependencies]
shadow-rs = "0.38.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,7 +31,6 @@ 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 = "0.38.0", features = ["metadata"] }
hickory-resolver = "0.24.1" hickory-resolver = "0.24.1"
toml = "0.8" toml = "0.8"
@@ -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"

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
@@ -144,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>
@@ -288,10 +283,10 @@ $ cargo run --release -- --frontend cli
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,
@@ -315,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
@@ -385,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.
@@ -405,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,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}");
} }

12
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1740560979, "lastModified": 1728018373,
"narHash": "sha256-Vr3Qi346M+8CjedtbyUevIGDZW8LcA1fTG0ugPY/Hic=", "narHash": "sha256-NOiTvBbRLIOe5F6RbHaAh6++BNjsb149fGZd1T4+KBg=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "5135c59491985879812717f4c9fea69604e7f26f", "rev": "bc947f541ae55e999ffdb4013441347d83b00feb",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -29,11 +29,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1740623427, "lastModified": 1728181869,
"narHash": "sha256-3SdPQrZoa4odlScFDUHd4CUPQ/R1gtH4Mq9u8CBiK8M=", "narHash": "sha256-sQXHXsjIcGEoIHkB+RO6BZdrPfB+43V1TEpyoWRI3ww=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "d342e8b5fd88421ff982f383c853f0fc78a847ab", "rev": "cd46aa3906c14790ef5cbe278d9e54f2c38f95c0",
"type": "github" "type": "github"
}, },
"original": { "original": {

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);
} }
} }
@@ -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

@@ -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
@@ -594,7 +594,7 @@ impl LanMouseInputCapture for LibeiInputCapture<'_> {
} }
} }
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 */
@@ -603,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

@@ -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,545 +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, 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::{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), HWND::default(), 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, HINSTANCE::default(), 0).unwrap();
let _ = SetWindowsHookExW(WH_KEYBOARD_LL, kybrd_proc, HINSTANCE::default(), 0).unwrap();
}
let instance = unsafe { GetModuleHandleW(None).unwrap() };
let window_class: WNDCLASSW = WNDCLASSW {
lpfnWndProc: window_proc,
hInstance: instance.into(),
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,
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 == 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(HHOOK::default(), 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(HHOOK::default(), 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 & 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,
});
}
}
}
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 },
})
}
w => {
log::warn!("unknown mouse event: {w:?}");
None
}
}
}

View File

@@ -25,7 +25,6 @@ tokio = { version = "1.32.0", features = [
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",

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

@@ -170,7 +170,7 @@ fn get_display_at_point(x: CGFloat, y: CGFloat) -> Option<CGDirectDisplayID> {
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) {

View File

@@ -2,12 +2,9 @@ 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;
@@ -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);
} }
@@ -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,7 +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 / 6., value / 120); .axis_discrete(0, axis, value as f64 / 6., value / 120);
self.pointer.frame(); self.pointer.frame();
} }
} }
@@ -219,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,
@@ -237,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);
} }
@@ -301,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!("{:#?} is not a modifier key", 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

@@ -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,7 +141,7 @@ 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] #[must_use]
#[allow(clippy::type_complexity, clippy::type_repetition_in_bounds)] #[allow(clippy::type_complexity, clippy::type_repetition_in_bounds)]

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, 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(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

@@ -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

@@ -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

@@ -9,10 +9,12 @@ 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;
@@ -47,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![];
@@ -64,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() {
@@ -125,28 +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::IncomingConnected(_fingerprint, addr, pos) => {
window.show_toast(format!("device connected: {addr} ({pos})").as_str());
} }
FrontendEvent::IncomingDisconnected(addr) => { FrontendEvent::Enumerate(clients) => {
window.show_toast(format!("{addr} disconnected").as_str()); for (handle, client, state) in clients {
if window.client_idx(handle).is_some() {
window.update_client_config(handle, client);
window.update_client_state(handle, state);
} else {
window.new_client(handle, client, state);
}
}
} }
FrontendEvent::PortChanged(port, msg) => {
match msg {
None => window.show_toast(format!("port changed: {port}").as_str()),
Some(msg) => window.show_toast(msg.as_str()),
}
window.imp().set_port(port);
}
FrontendEvent::CaptureStatus(s) => {
window.set_capture(s.into());
}
FrontendEvent::EmulationStatus(s) => {
window.set_emulation(s.into());
}
FrontendEvent::AuthorizedUpdated(keys) => {
window.set_authorized_keys(keys);
}
FrontendEvent::PublicKeyFingerprint(fp) => {
window.set_pk_fp(&fp);
}
FrontendEvent::IncomingConnected(..) => {}
FrontendEvent::IncomingDisconnected(..) => {}
} }
} }
} }

View File

@@ -8,7 +8,7 @@ use glib::{clone, Object};
use gtk::{ use gtk::{
gio, gio,
glib::{self, closure_local}, glib::{self, closure_local},
NoSelection, ListBox, NoSelection,
}; };
use lan_mouse_ipc::{ use lan_mouse_ipc::{
@@ -28,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()
@@ -38,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()
@@ -46,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()
@@ -62,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));
@@ -120,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::info!("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::info!( 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,
));
} }
} }
), ),
@@ -183,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);
} }
} }
), ),
@@ -196,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,
));
} }
} }
), ),
@@ -231,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 {
@@ -247,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 {
@@ -256,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);
@@ -268,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;
@@ -319,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
@@ -354,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
@@ -382,19 +303,55 @@ 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) { pub fn request_dns(&self, client: &ClientObject) {
self.request(FrontendRequest::ResolveDns(client.get_data().handle));
}
pub fn request_client_update(&self, client: &ClientObject) {
let handle = client.handle();
let data = client.get_data();
let position = Position::try_from(data.position.as_str()).expect("invalid position");
let hostname = data.hostname;
let port = data.port as u16;
for event in [
FrontendRequest::UpdateHostname(handle, hostname),
FrontendRequest::UpdatePosition(handle, position),
FrontendRequest::UpdatePort(handle, port),
] {
self.request(event);
}
}
pub fn request_client_activate(&self, client: &ClientObject, active: bool) {
self.request(FrontendRequest::Activate(client.handle(), active));
}
pub fn request_client_delete(&self, client: &ClientObject) {
self.request(FrontendRequest::Delete(client.handle()));
}
pub fn open_fingerprint_dialog(&self) {
let window = FingerprintWindow::new(); let window = FingerprintWindow::new();
window.set_transient_for(Some(self)); window.set_transient_for(Some(self));
window.connect_closure( window.connect_closure(
@@ -412,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) {
@@ -428,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();
} }
@@ -454,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();
@@ -466,7 +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);
} }
} }

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)]
@@ -179,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
@@ -229,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
@@ -239,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

@@ -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,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,13 +1,9 @@
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, 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());

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,10 +17,6 @@ use input_event::scancode::{
Linux::{KeyLeftAlt, KeyLeftCtrl, KeyLeftMeta, KeyLeftShift}, Linux::{KeyLeftAlt, KeyLeftCtrl, KeyLeftMeta, KeyLeftShift},
}; };
use shadow_rs::shadow;
shadow!(build);
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct ConfigToml { pub struct ConfigToml {
pub capture_backend: Option<CaptureBackend>, pub capture_backend: Option<CaptureBackend>,
@@ -41,6 +34,7 @@ pub struct ConfigToml {
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct TomlClient { pub struct TomlClient {
pub capture_backend: Option<CaptureBackend>,
pub hostname: Option<String>, pub hostname: Option<String>,
pub host_name: Option<String>, pub host_name: Option<String>,
pub ips: Option<Vec<IpAddr>>, pub ips: Option<Vec<IpAddr>>,
@@ -57,8 +51,8 @@ impl ConfigToml {
} }
#[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)]
pub 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>,
@@ -69,34 +63,31 @@ pub struct Args {
/// non-default config file location /// non-default config file location
#[arg(short, long)] #[arg(short, long)]
pub config: Option<PathBuf>, config: Option<String>,
#[command(subcommand)] /// run only the service as a daemon without the frontend
pub command: Option<Command>, #[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)]
pub capture_backend: Option<CaptureBackend>, capture_backend: Option<CaptureBackend>,
/// emulation backend override /// emulation backend override
#[arg(long)] #[arg(long)]
pub emulation_backend: Option<EmulationBackend>, emulation_backend: Option<EmulationBackend>,
/// path to non-default certificate location /// path to non-default certificate location
#[arg(long)] #[arg(long)]
pub cert_path: Option<PathBuf>, cert_path: Option<PathBuf>,
}
#[derive(Subcommand, 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)]
@@ -224,8 +215,8 @@ impl Display for EmulationBackend {
pub enum Frontend { pub enum Frontend {
#[serde(rename = "gtk")] #[serde(rename = "gtk")]
Gtk, Gtk,
#[serde(rename = "none")] #[serde(rename = "cli")]
None, Cli,
} }
impl Default for Frontend { impl Default for Frontend {
@@ -233,7 +224,7 @@ impl Default for Frontend {
if cfg!(feature = "gtk") { if cfg!(feature = "gtk") {
Self::Gtk Self::Gtk
} else { } else {
Self::None Self::Cli
} }
} }
} }
@@ -254,6 +245,8 @@ pub struct Config {
pub port: u16, pub port: u16,
/// list of clients /// list of clients
pub clients: Vec<(TomlClient, Position)>, pub clients: Vec<(TomlClient, Position)>,
/// whether or not to run as a daemon
pub daemon: bool,
/// configured release bind /// configured release bind
pub release_bind: Vec<scancode::Linux>, pub release_bind: Vec<scancode::Linux>,
/// test capture instead of running the app /// test capture instead of running the app
@@ -287,7 +280,8 @@ const DEFAULT_RELEASE_KEYS: [scancode::Linux; 4] =
[KeyLeftCtrl, KeyLeftShift, KeyLeftMeta, KeyLeftAlt]; [KeyLeftCtrl, KeyLeftShift, KeyLeftMeta, KeyLeftAlt];
impl Config { impl Config {
pub fn new(args: &Args) -> Result<Self, ConfigError> { pub fn new() -> Result<Self, ConfigError> {
let args = CliArgs::parse();
const CONFIG_FILE_NAME: &str = "config.toml"; const CONFIG_FILE_NAME: &str = "config.toml";
const CERT_FILE_NAME: &str = "lan-mouse.pem"; const CERT_FILE_NAME: &str = "lan-mouse.pem";
@@ -309,7 +303,7 @@ impl Config {
let config_file = config_path.join(CONFIG_FILE_NAME); let config_file = config_path.join(CONFIG_FILE_NAME);
// --config <file> overrules default location // --config <file> overrules default location
let config_file = args.config.clone().unwrap_or(config_file); let config_file = args.config.map(PathBuf::from).unwrap_or(config_file);
let mut config_toml = match ConfigToml::new(&config_file) { let mut config_toml = match ConfigToml::new(&config_file) {
Err(e) => { Err(e) => {
@@ -345,7 +339,6 @@ impl Config {
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(config_path.join(CERT_FILE_NAME)); .unwrap_or(config_path.join(CERT_FILE_NAME));
@@ -371,14 +364,16 @@ impl Config {
} }
} }
let test_capture = matches!(args.command, Some(Command::TestCapture(_))); let daemon = args.daemon;
let test_emulation = matches!(args.command, Some(Command::TestEmulation(_))); let test_capture = args.test_capture;
let test_emulation = args.test_emulation;
Ok(Config { Ok(Config {
path: config_path, path: config_path,
authorized_fingerprints, authorized_fingerprints,
capture_backend, capture_backend,
emulation_backend, emulation_backend,
daemon,
frontend, frontend,
clients, clients,
port, port,

View File

@@ -1,4 +1,4 @@
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};
@@ -30,7 +30,6 @@ struct DnsTask {
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 {
@@ -40,7 +39,6 @@ impl DnsResolver {
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,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,17 +7,7 @@ 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, 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());

View File

@@ -1,14 +1,12 @@
use clap::Parser;
use env_logger::Env; use env_logger::Env;
use input_capture::InputCaptureError; use input_capture::InputCaptureError;
use input_emulation::InputEmulationError; use input_emulation::InputEmulationError;
use lan_mouse::{ use lan_mouse::{
capture_test, capture_test,
config::{self, Config, ConfigError, Frontend}, config::{Config, ConfigError, Frontend},
emulation_test, emulation_test,
service::{Service, ServiceError}, service::{Service, ServiceError},
}; };
use lan_mouse_cli::CliError;
use lan_mouse_ipc::{IpcError, IpcListenerCreationError}; use lan_mouse_ipc::{IpcError, IpcListenerCreationError};
use std::{ use std::{
future::Future, future::Future,
@@ -32,8 +30,6 @@ enum LanMouseError {
Capture(#[from] InputCaptureError), Capture(#[from] InputCaptureError),
#[error(transparent)] #[error(transparent)]
Emulation(#[from] InputEmulationError), Emulation(#[from] InputEmulationError),
#[error(transparent)]
Cli(#[from] CliError),
} }
fn main() { fn main() {
@@ -49,39 +45,34 @@ fn main() {
fn run() -> Result<(), LanMouseError> { fn run() -> Result<(), LanMouseError> {
// parse config file + cli args // parse config file + cli args
let args = config::Args::parse(); let config = Config::new()?;
let config = config::Config::new(&args)?; if config.test_capture {
match args.command { run_async(capture_test::run(config))?;
Some(command) => match command { } else if config.test_emulation {
config::Command::TestEmulation(args) => run_async(emulation_test::run(config, args))?, run_async(emulation_test::run(config))?;
config::Command::TestCapture(args) => run_async(capture_test::run(config, args))?, } else if config.daemon {
config::Command::Cli(cli_args) => run_async(lan_mouse_cli::run(cli_args))?, // if daemon is specified we run the service
config::Command::Daemon => { match run_async(run_service(config)) {
// if daemon is specified we run the service Err(LanMouseError::Service(ServiceError::IpcListen(
match run_async(run_service(config)) { IpcListenerCreationError::AlreadyRunning,
Err(LanMouseError::Service(ServiceError::IpcListen( ))) => log::info!("service already running!"),
IpcListenerCreationError::AlreadyRunning, r => r?,
))) => log::info!("service already running!"),
r => r?,
}
}
},
None => {
// 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()?;
} }
} 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(())
@@ -105,7 +96,7 @@ where
fn start_service() -> Result<Child, io::Error> { fn start_service() -> Result<Child, io::Error> {
let child = 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)
} }
@@ -127,8 +118,8 @@ fn run_frontend(config: &Config) -> Result<(), IpcError> {
} }
#[cfg(not(feature = "gtk"))] #[cfg(not(feature = "gtk"))]
Frontend::Gtk => panic!("gtk frontend requested but feature not enabled!"), Frontend::Gtk => panic!("gtk frontend requested but feature not enabled!"),
Frontend::None => { Frontend::Cli => {
log::warn!("no frontend available!"); lan_mouse_cli::run()?;
} }
}; };
Ok(()) Ok(())

View File

@@ -186,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),
@@ -193,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)
}
} }
} }
@@ -285,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) {
@@ -401,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}");
} }
} }
@@ -427,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})");
} }
} }
@@ -454,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) {
@@ -476,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) {