Compare commits

...

42 Commits

Author SHA1 Message Date
Ferdinand Schober
1c69d4d209 macos: fix scroll capture 2025-11-03 17:19:15 +01:00
NeoTheFox
3f13714d8a Add rustfmt.toml for explicit styling (#348)
* Propose an explicit .rustfnt.toml
Use 2024 style, 4 spaces for tabs and epand the default width a tad

* Auto-format the existing code with new rules
2025-11-02 11:52:01 +01:00
Ferdinand Schober
3483d242e2 fix inconsistent mouse capture on macos (#346) 2025-10-31 14:43:28 +01:00
Ferdinand Schober
35773dfd07 macos: fix modifier capture (#342) 2025-10-30 20:16:27 +01:00
Ferdinand Schober
f91b6bd3c1 macos: reset double click when mouse is moved (#341) 2025-10-30 00:48:24 +01:00
Ferdinand Schober
2d1a037eba macos: fix duplicated key release event (#340) 2025-10-29 18:37:24 +01:00
Ferdinand Schober
057f6e2567 macos: emulate double / triple click (#338) 2025-10-29 17:46:15 +01:00
Ferdinand Schober
99c8bc5567 macsos: use ScrollEventUnit::LINE for mousewheel (#337) 2025-10-29 16:18:46 +01:00
Ferdinand Schober
0dd413e989 prevent authorization request spamming windows (#335) 2025-10-28 07:25:01 +01:00
Ferdinand Schober
4e5a66340a Partially Revert "slow scrolling chrome with emulation=wlroots capture=layer-shell (#318) (#325)" (#334)
The division by 120 was correct.
2025-10-27 15:56:30 +01:00
Micah R Ledbetter
0a0d91b0da Include libadwaita and other dependencies in the app bundle on macOS (#271)
* Include libadwaita and other dependencies in the app bundle on macOS

* Fix missing pipes

* Use recent bash for associative array support (declare -A)

* Use correct path for homebrew bash on Intel macOS

* Get homebrew path from the brew command

* Simplify copy-macos-dylib and convert to POSIX sh

Remove need for recent bash altogether

* Fix permissions nit

* Update macOS dylib copy script path in release workflow

* fix a few typos

* fix script invocation in pre-release.yml

---------

Co-authored-by: Apoorv Khandelwal <mail@apoorvkh.com>
Co-authored-by: Ferdinand Schober <ferdinandschober20@gmail.com>
2025-10-14 13:35:07 +02:00
ayykamp
94e6372218 Add development flatpak manifest (#328) 2025-10-12 15:09:41 +02:00
Thomas Matthijs
39b79d88a5 slow scrolling chrome with emulation=wlroots capture=layer-shell (#318) (#325)
Using niri as compositor on both sides resulting in: emulation=wlroots capture=layer-shell

Mouse scrolling works fine in terminals, but only scrolls very small amount in google-chrome (in wayland mode)

Using 'wev' to show events, using the real mouse shows

[        15:      wl_pointer] axis_source: 0 (wheel)
[        15:      wl_pointer] axis_value120: axis: 0 (vertical), value120: 120
[        15:      wl_pointer] axis_relative_direction: axis: 0 (vertical), direction: 0
[        15:      wl_pointer] axis: time: 50410752; axis: 0 (vertical), value: 15.000000

Using the lan-mouse shows:

[        15:      wl_pointer] axis_source: 2 (continuous)
[        15:      wl_pointer] axis_value120: axis: 0 (vertical), value120: 1
[        15:      wl_pointer] axis_relative_direction: axis: 0 (vertical), direction: 0
[        15:      wl_pointer] axis: time: -1913142096; axis: 0 (vertical), value: 20.000000

Without axis_source, scrolling over (pinned) tabs also skips one tab.
2025-10-09 00:28:26 +02:00
ayykamp
5ad90ca6a5 doc: add missing closing tag in README (#326) 2025-10-08 19:54:49 +02:00
ayykamp
68df27ab2c doc: add instructions to install from terra repo on fedora (#308) 2025-10-08 17:12:13 +02:00
Ferdinand Schober
eb1dcbddb0 update dependencies (#302)
* update dependencies

* update windows

* clippy: inline format args

* update flake

* update core-graphics

* fix poll after completion error

* fix ashpd?!
2025-10-08 16:10:32 +02:00
Ferdinand Schober
9f10ebcbd2 Macos cleanup event thread (#324) 2025-10-08 02:00:37 +02:00
Ferdinand Schober
e29eb7134c macos: fix a crash when InputCapture is dropped (#323) 2025-10-08 00:22:52 +02:00
Ferdinand Schober
e46fe60b3e fix parent class types in key_row widget (#300)
closes #294
2025-06-12 18:17:36 +02:00
Ferdinand Schober
37a4e236b8 fix clippy warnings from rust 1.87 (#301) 2025-06-12 17:52:23 +02:00
Leon Linhart
b8063a8138 Capture horizontal scroll on Windows (#283) 2025-04-02 02:39:49 +02:00
Ferdinand Schober
5a3a21c2c0 clients should not be mandatory in configuration (#285)
closes #284
2025-04-01 13:22:08 +02:00
Ferdinand Schober
3ec23d7171 unauthorized device accept notification (#282)
* ask the user to accept unauthorized devices

* only alert on actual error
2025-03-22 22:50:19 +01:00
Michel Lao
15296263b2 Fix parsing TOML key 'position' and values (#281)
* fix parsing toml key position and values

* Using rename_all instead rename over each enum

* rename struct field directly

---------

Co-authored-by: Ferdinand Schober <ferdinand.schober@fau.de>
2025-03-21 14:02:38 +01:00
Ferdinand Schober
5736919f89 Update README.md
update cli interface usage
2025-03-16 00:36:21 +01:00
Ferdinand Schober
1ece2a417d Update README.md 2025-03-15 18:48:25 +01:00
Ferdinand Schober
e101ff281b Update config.toml 2025-03-15 18:48:05 +01:00
Ferdinand Schober
532383ef65 Update README.md 2025-03-15 18:47:18 +01:00
Ferdinand Schober
92f652df2e feat: simplify and change configuration (#279)
*breaking change*
this changes the configuration syntax, allowing for an unlimited amount of configured clients.
Also a first step towards enabling a "save config" feature.
2025-03-15 18:45:19 +01:00
Ferdinand Schober
2f6a3629ad remove cli frontend in favour of cli subcommand (#278)
this removes the cli frontend entirely, replacing it with a subcommand instead
2025-03-15 18:20:25 +01:00
Ferdinand Schober
7898f2362c Update README.md (#277) 2025-03-15 16:51:23 +01:00
Ferdinand Schober
50a778452e Gtk frontend rework (#276)
client configuration now applies immediately instead of after enabling / disabling clients.
Also fixes a potential feedback loop when changing settings.
2025-03-15 01:21:53 +01:00
Ferdinand Schober
f247300f8c cancel previous dns request if a new one is made (#275) 2025-03-14 23:07:21 +01:00
Micah R Ledbetter
5d1745a60c Add cmd-q shortcut on macOS (#270) 2025-02-28 15:28:35 +01:00
Ferdinand Schober
615c75817a fix clippy lint 2025-02-27 16:57:53 +01:00
Ferdinand Schober
03407b9826 update flake 2025-02-27 16:57:38 +01:00
Ferdinand Schober
89684e1481 fix names in pre release as well 2025-02-21 13:45:00 +01:00
Ferdinand Schober
a1d4effcf9 fix file names 2025-02-21 13:28:52 +01:00
Ferdinand Schober
da054b7a9a fix bundle path 2025-02-21 12:45:54 +01:00
Micah R Ledbetter
508d066700 Build a macOS bundle for Intel and ARM (#266)
* Build a macOS bundle for Intel and ARM

* Build icon.icns file in a script

* Add imagemagick

* Add macOS bundling to pre/tagged-release actions
2025-02-21 12:35:13 +01:00
Ferdinand Schober
21c24f7fa1 layer-shell: handle added/removed globals
closes #253
2025-02-13 22:10:12 +01:00
Ferdinand Schober
3e1c3e95b7 use shadow-rs instead of executing git describe
this removes git from the build dependencies
2025-01-27 16:51:25 +01:00
72 changed files with 3729 additions and 2593 deletions

View File

@@ -84,36 +84,60 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: install dependencies - name: install dependencies
run: brew install gtk4 libadwaita run: brew install gtk4 libadwaita imagemagick
- name: Release Build - name: Release Build
run: | run: |
cargo build --release cargo build --release
cp target/release/lan-mouse lan-mouse-macos-intel cp target/release/lan-mouse lan-mouse-macos-intel
- name: Make icns
run: scripts/makeicns.sh
- name: Install cargo bundle
run: cargo install cargo-bundle
- name: Bundle
run: |
cargo bundle --release
scripts/copy-macos-dylib.sh "target/release/bundle/osx/Lan Mouse.app/Contents/MacOS/lan-mouse"
- name: Zip bundle
run: |
cd target/release/bundle/osx
zip -r "lan-mouse-macos-intel.zip" "Lan Mouse.app"
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: lan-mouse-macos-intel name: lan-mouse-macos-intel
path: lan-mouse-macos-intel path: target/release/bundle/osx/lan-mouse-macos-intel.zip
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 run: brew install gtk4 libadwaita imagemagick
- name: Release Build - name: Release Build
run: | run: |
cargo build --release cargo build --release
cp target/release/lan-mouse lan-mouse-macos-aarch64 cp target/release/lan-mouse lan-mouse-macos-aarch64
- name: Make icns
run: scripts/makeicns.sh
- name: Install cargo bundle
run: cargo install cargo-bundle
- name: Bundle
run: |
cargo bundle --release
scripts/copy-macos-dylib.sh "target/release/bundle/osx/Lan Mouse.app/Contents/MacOS/lan-mouse"
- name: Zip bundle
run: |
cd target/release/bundle/osx
zip -r "lan-mouse-macos-aarch64.zip" "Lan Mouse.app"
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: lan-mouse-macos-aarch64 name: lan-mouse-macos-aarch64
path: lan-mouse-macos-aarch64 path: target/release/bundle/osx/lan-mouse-macos-aarch64.zip
pre-release: pre-release:
name: "Pre Release" name: "Pre Release"
needs: [windows-release-build, linux-release-build, macos-release-build] needs: [windows-release-build, linux-release-build, macos-release-build, macos-aarch64-release-build]
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"
steps: steps:
- name: Download build artifacts - name: Download build artifacts
@@ -127,6 +151,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 lan-mouse-macos-intel/lan-mouse-macos-intel.zip
lan-mouse-macos-aarch64/lan-mouse-macos-aarch64 lan-mouse-macos-aarch64/lan-mouse-macos-aarch64.zip
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 run: brew install gtk4 libadwaita imagemagick
- name: Build - name: Build
run: cargo build --verbose run: cargo build --verbose
- name: Run tests - name: Run tests
@@ -107,18 +107,30 @@ jobs:
run: cargo fmt --check run: cargo fmt --check
- name: Clippy - name: Clippy
run: cargo clippy --all-features --all-targets -- --deny warnings run: cargo clippy --all-features --all-targets -- --deny warnings
- name: Make icns
run: scripts/makeicns.sh
- name: Install cargo bundle
run: cargo install cargo-bundle
- name: Bundle
run: |
cargo bundle
scripts/copy-macos-dylib.sh
- name: Zip bundle
run: |
cd target/debug/bundle/osx
zip -r "Lan Mouse macOS (Intel).zip" "Lan Mouse.app"
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: lan-mouse-macos name: Lan Mouse macOS (Intel)
path: target/debug/lan-mouse path: target/debug/bundle/osx/Lan Mouse macOS (Intel).zip
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 run: brew install gtk4 libadwaita imagemagick
- name: Build - name: Build
run: cargo build --verbose run: cargo build --verbose
- name: Run tests - name: Run tests
@@ -127,8 +139,20 @@ jobs:
run: cargo fmt --check run: cargo fmt --check
- name: Clippy - name: Clippy
run: cargo clippy --all-features --all-targets -- --deny warnings run: cargo clippy --all-features --all-targets -- --deny warnings
- name: Make icns
run: scripts/makeicns.sh
- name: Install cargo bundle
run: cargo install cargo-bundle
- name: Bundle
run: |
cargo bundle
scripts/copy-macos-dylib.sh
- name: Zip bundle
run: |
cd target/debug/bundle/osx
zip -r "Lan Mouse macOS (ARM).zip" "Lan Mouse.app"
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: lan-mouse-macos-aarch64 name: Lan Mouse macOS (ARM)
path: target/debug/lan-mouse path: target/debug/bundle/osx/Lan Mouse macOS (ARM).zip

View File

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

4
.gitignore vendored
View File

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

4
.rustfmt.toml Normal file
View File

@@ -0,0 +1,4 @@
style_edition = "2024"
max_width = 100
tab_spaces = 4

2241
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,9 @@ lto = "fat"
strip = true strip = true
panic = "abort" panic = "abort"
[build-dependencies]
shadow-rs = "1.2.0"
[dependencies] [dependencies]
input-event = { path = "input-event", version = "0.3.0" } input-event = { path = "input-event", version = "0.3.0" }
input-emulation = { path = "input-emulation", version = "0.3.0", default-features = false } input-emulation = { path = "input-emulation", version = "0.3.0", default-features = false }
@@ -31,8 +34,9 @@ lan-mouse-cli = { path = "lan-mouse-cli", version = "0.2.0" }
lan-mouse-gtk = { path = "lan-mouse-gtk", version = "0.2.0", optional = true } lan-mouse-gtk = { path = "lan-mouse-gtk", version = "0.2.0", optional = true }
lan-mouse-ipc = { path = "lan-mouse-ipc", version = "0.2.0" } lan-mouse-ipc = { path = "lan-mouse-ipc", version = "0.2.0" }
lan-mouse-proto = { path = "lan-mouse-proto", version = "0.2.0" } lan-mouse-proto = { path = "lan-mouse-proto", version = "0.2.0" }
shadow-rs = { version = "1.2.0", features = ["metadata"] }
hickory-resolver = "0.24.1" hickory-resolver = "0.25.2"
toml = "0.8" toml = "0.8"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
log = "0.4.20" log = "0.4.20"
@@ -54,8 +58,8 @@ slab = "0.4.9"
thiserror = "2.0.0" thiserror = "2.0.0"
tokio-util = "0.7.11" tokio-util = "0.7.11"
local-channel = "0.1.5" local-channel = "0.1.5"
webrtc-dtls = { version = "0.10.0", features = ["pem"] } webrtc-dtls = { version = "0.12.0", features = ["pem"] }
webrtc-util = "0.9.0" webrtc-util = "0.11.0"
rustls = { version = "0.23.12", default-features = false, features = [ rustls = { version = "0.23.12", default-features = false, features = [
"std", "std",
"ring", "ring",
@@ -85,3 +89,8 @@ 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

@@ -81,15 +81,37 @@ paru -S lan-mouse-git
- flake: [README.md](./nix/README.md) - flake: [README.md](./nix/README.md)
</details> </details>
<details>
<summary>Fedora</summary>
You can install Lan Mouse from the [Terra Repository](https://terra.fyralabs.com).
After enabling Terra:
```sh
dnf install lan-mouse
```
</details>
<details>
<summary>MacOS</summary>
- Download the package for your Mac (Intel or ARM) from the releases page
- Unzip it
- Remove the quarantine with `xattr -rd com.apple.quarantine "Lan Mouse.app"`
- Launch the app
- Grant accessibility permissions in System Preferences
</details>
<details> <details>
<summary>Manual Installation</summary> <summary>Manual Installation</summary>
First make sure to [install the necessary dependencies](#installing-dependencies). First make sure to [install the necessary dependencies](#installing-dependencies-for-development--compiling-from-source).
Precompiled release binaries for Windows, MacOS and Linux are available in the [releases section](https://github.com/feschber/lan-mouse/releases). Precompiled release binaries for Windows, MacOS and Linux are available in the [releases section](https://github.com/feschber/lan-mouse/releases).
For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies). For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies-for-development--compiling-from-source).
Alternatively, the `lan-mouse` binary can be compiled from source (see below). Alternatively, the `lan-mouse` binary can be compiled from source (see below).
@@ -144,10 +166,11 @@ 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 wayland support is needed, the following command produces E.g. if only support for sway is needed, the following command produces
an executable with just support for wayland: an executable with support for only the `layer-shell` capture backend
and `wlroots` emulation backend:
```sh ```sh
cargo build --no-default-features --features wayland cargo build --no-default-features --features layer_shell_capture,wlroots_emulation
``` ```
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>
@@ -160,7 +183,15 @@ For a detailed list of available features, checkout the [Cargo.toml](./Cargo.tom
<summary>MacOS</summary> <summary>MacOS</summary>
```sh ```sh
brew install libadwaita pkg-config # Install dependencies
brew install libadwaita pkg-config imagemagick
cargo install cargo-bundle
# Create the macOS icon file
scripts/makeicns.sh
# Create the .app bundle
cargo bundle
# Copy all dynamic libraries into the bundle, and update the bundle to find them there
scripts/copy-macos-dylib.sh
``` ```
</details> </details>
@@ -267,19 +298,17 @@ If the device still can not be entered, make sure you have UDP port `4242` (or t
<details> <details>
<summary>Command Line Interface</summary> <summary>Command Line Interface</summary>
The cli interface can be enabled using `--frontend cli` as commandline arguments. The cli interface can be accessed by passing `cli` as a commandline argument.
Type `help` to list the available commands. Use
E.g.:
```sh ```sh
$ cargo run --release -- --frontend cli lan-mouse cli help
(...)
> connect <host> left|right|top|bottom
(...)
> list
(...)
> activate 0
``` ```
to list the available commands and
```sh
lan-mouse cli <cmd> help
```
for information on how to use a specific command.
</details> </details>
<details> <details>
@@ -287,10 +316,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, add `--daemon` to the commandline args: To do so, use the `daemon` subcommand:
```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,
@@ -325,9 +354,6 @@ release_bind = [ "KeyA", "KeyS", "KeyD", "KeyF" ]
# optional port (defaults to 4242) # optional port (defaults to 4242)
port = 4242 port = 4242
# # optional frontend -> defaults to gtk if available
# # possible values are "cli" and "gtk"
# frontend = "gtk"
# list of authorized tls certificate fingerprints that # list of authorized tls certificate fingerprints that
# are accepted for incoming traffic # are accepted for incoming traffic
@@ -335,7 +361,9 @@ port = 4242
"bc:05:ab:7a:a4:de:88:8c:2f:92:ac:bc:b8:49:b8:24:0d:44:b3:e6:a4:ef:d7:0b:6c:69:6d:77:53:0b:14:80" = "iridium" "bc:05:ab:7a:a4:de:88:8c:2f:92:ac:bc:b8:49:b8:24:0d:44:b3:e6:a4:ef:d7:0b:6c:69:6d:77:53:0b:14:80" = "iridium"
# define a client on the right side with host name "iridium" # define a client on the right side with host name "iridium"
[right] [[clients]]
# position (left | right | top | bottom)
position = "right"
# hostname # hostname
hostname = "iridium" hostname = "iridium"
# activate this client immediately when lan-mouse is started # activate this client immediately when lan-mouse is started
@@ -344,7 +372,8 @@ activate_on_startup = true
ips = ["192.168.178.156"] ips = ["192.168.178.156"]
# define a client on the left side with IP address 192.168.178.189 # define a client on the left side with IP address 192.168.178.189
[left] [[clients]]
position = "left"
# The hostname is optional: When no hostname is specified, # The hostname is optional: When no hostname is specified,
# at least one ip address needs to be specified. # at least one ip address needs to be specified.
hostname = "thorium" hostname = "thorium"

View File

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

View File

@@ -1,20 +1,8 @@
use std::process::Command; use shadow_rs::ShadowBuilder;
fn main() { fn main() {
// commit hash ShadowBuilder::builder()
let git_describe = Command::new("git") .deny_const(Default::default())
.arg("describe") .build()
.arg("--always") .expect("shadow build");
.arg("--dirty")
.arg("--tags")
.output()
.map(|output| String::from_utf8(output.stdout).ok())
.ok()
.flatten()
.unwrap_or_else(|| {
println!("cargo:warning=Failed to get git describe");
String::from("unknown")
});
let git_describe = git_describe.trim().to_string();
println!("cargo::rustc-env=GIT_DESCRIBE={git_describe}");
} }

View File

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

View File

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

1
dylibs/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*

12
flake.lock generated
View File

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

View File

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

View File

@@ -1,6 +1,6 @@
use std::f64::consts::PI; use std::f64::consts::PI;
use std::pin::Pin; use std::pin::Pin;
use std::task::{ready, Context, Poll}; use std::task::{Context, Poll, ready};
use std::time::Duration; use std::time::Duration;
use async_trait::async_trait; use async_trait::async_trait;

View File

@@ -12,9 +12,9 @@ pub enum InputCaptureError {
use std::io; use std::io;
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))] #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
use wayland_client::{ use wayland_client::{
ConnectError, DispatchError,
backend::WaylandError, backend::WaylandError,
globals::{BindError, GlobalError}, globals::{BindError, GlobalError},
ConnectError, DispatchError,
}; };
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]

View File

@@ -1,12 +1,13 @@
use async_trait::async_trait; use async_trait::async_trait;
use futures_core::Stream; use futures_core::Stream;
use std::{ use std::{
collections::VecDeque, collections::{HashSet, VecDeque},
env, env,
fmt::{self, Display},
io::{self, ErrorKind}, io::{self, ErrorKind},
os::fd::{AsFd, RawFd}, os::fd::{AsFd, RawFd},
pin::Pin, pin::Pin,
task::{ready, Context, Poll}, task::{Context, Poll, ready},
}; };
use tokio::io::unix::AsyncFd; use tokio::io::unix::AsyncFd;
@@ -44,18 +45,20 @@ use wayland_protocols_wlr::layer_shell::v1::client::{
}; };
use wayland_client::{ use wayland_client::{
Connection, Dispatch, DispatchError, EventQueue, QueueHandle, WEnum,
backend::{ReadEventsGuard, WaylandError}, backend::{ReadEventsGuard, WaylandError},
delegate_noop, delegate_noop,
globals::{registry_queue_init, GlobalListContents}, globals::{Global, GlobalList, GlobalListContents, registry_queue_init},
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_registry, wl_seat, wl_shm, wl_shm_pool, wl_region,
wl_registry::{self, WlRegistry},
wl_seat, wl_shm, wl_shm_pool,
wl_surface::WlSurface, wl_surface::WlSurface,
}, },
Connection, Dispatch, DispatchError, EventQueue, QueueHandle, WEnum,
}; };
use input_event::{Event, KeyboardEvent, PointerEvent}; use input_event::{Event, KeyboardEvent, PointerEvent};
@@ -63,8 +66,8 @@ use input_event::{Event, KeyboardEvent, PointerEvent};
use crate::{CaptureError, CaptureEvent}; use crate::{CaptureError, CaptureEvent};
use super::{ use super::{
error::{LayerShellCaptureCreationError, WaylandBindError},
Capture, Position, Capture, Position,
error::{LayerShellCaptureCreationError, WaylandBindError},
}; };
struct Globals { struct Globals {
@@ -75,28 +78,42 @@ 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(Debug, Clone)] #[derive(Clone, Debug)]
struct Output {
wl_output: WlOutput,
global: Global,
info: Option<OutputInfo>,
pending_info: OutputInfo,
has_xdg_info: bool,
}
impl Display for Output {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(info) = &self.info {
write!(
f,
"{} {}x{} @pos {:?} ({})",
info.name, info.size.0, info.size.1, info.position, info.description
)
} else {
write!(f, "unknown output")
}
}
}
#[derive(Clone, Debug, Default)]
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>,
@@ -104,12 +121,13 @@ 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>>,
g: Globals, global_list: GlobalList,
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)>,
output_info: Vec<(WlOutput, OutputInfo)>, outputs: Vec<Output>,
scroll_discrete_pending: bool, scroll_discrete_pending: bool,
} }
@@ -142,7 +160,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.g; let g = &state.globals;
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),
@@ -203,41 +221,36 @@ impl Drop for Window {
} }
} }
fn get_edges(outputs: &[(WlOutput, OutputInfo)], pos: Position) -> Vec<(WlOutput, i32)> { fn get_edges(outputs: &[Output], pos: Position) -> Vec<(Output, i32)> {
outputs outputs
.iter() .iter()
.map(|(o, i)| { .filter_map(|output| {
( output.info.as_ref().map(|info| {
o.clone(), (
match pos { output.clone(),
Position::Left => i.position.0, match pos {
Position::Right => i.position.0 + i.size.0, Position::Left => info.position.0,
Position::Top => i.position.1, Position::Right => info.position.0 + info.size.0,
Position::Bottom => i.position.1 + i.size.1, Position::Top => info.position.1,
}, Position::Bottom => info.position.1 + info.size.1,
) },
)
})
}) })
.collect() .collect()
} }
fn get_output_configuration(state: &State, pos: Position) -> Vec<(WlOutput, OutputInfo)> { fn get_output_configuration(state: &State, pos: Position) -> Vec<Output> {
// get all output edges corresponding to the position // get all output edges corresponding to the position
let edges = get_edges(&state.output_info, pos); let edges = get_edges(&state.outputs, pos);
log::debug!("edges: {edges:?}"); let opposite_edges = get_edges(&state.outputs, pos.opposite());
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
let outputs: Vec<WlOutput> = edges 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()
} }
@@ -259,36 +272,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 (g, mut queue) = registry_queue_init::<State>(&conn)?; let (global_list, mut queue) = registry_queue_init::<State>(&conn)?;
let qh = queue.handle(); let qh = queue.handle();
let compositor: wl_compositor::WlCompositor = g let compositor: wl_compositor::WlCompositor = global_list
.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 = g let xdg_output_manager: ZxdgOutputManagerV1 = global_list
.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 = g let shm: wl_shm::WlShm = global_list
.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 = g let layer_shell: ZwlrLayerShellV1 = global_list
.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 = g let seat: wl_seat::WlSeat = global_list
.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 = g let pointer_constraints: ZwpPointerConstraintsV1 = global_list
.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 = g let relative_pointer_manager: ZwpRelativePointerManagerV1 = global_list
.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,
> = g > = global_list
.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
@@ -297,65 +310,41 @@ 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,
g, global_list,
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, wayland_fd: queue.as_fd().as_raw_fd(),
read_guard: None, read_guard: None,
pending_events: VecDeque::new(), pending_events: VecDeque::new(),
output_info: vec![], outputs: vec![],
scroll_discrete_pending: false, scroll_discrete_pending: false,
}; };
// dispatch registry to () again, in order to read all wl_outputs for global in state.global_list.contents().clone_list() {
conn.display().get_registry(&state.qh, ()); state.register_global(global);
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());
} }
// roundtrip to read xdg_output events // flush outgoing events
queue.roundtrip(&mut state)?; queue.flush()?;
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() {
@@ -379,6 +368,7 @@ 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);
@@ -388,6 +378,52 @@ 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,
@@ -408,7 +444,7 @@ impl State {
// lock pointer // lock pointer
if self.pointer_lock.is_none() { if self.pointer_lock.is_none() {
self.pointer_lock = Some(self.g.pointer_constraints.lock_pointer( self.pointer_lock = Some(self.globals.pointer_constraints.lock_pointer(
surface, surface,
pointer, pointer,
None, None,
@@ -420,7 +456,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.g.relative_pointer_manager.get_relative_pointer( self.rel_pointer = Some(self.globals.relative_pointer_manager.get_relative_pointer(
pointer, pointer,
qh, qh,
(), (),
@@ -428,10 +464,14 @@ impl State {
} }
// capture modifier keys // capture modifier keys
if let Some(shortcut_inhibit_manager) = &self.g.shortcut_inhibit_manager { if let Some(shortcut_inhibit_manager) = &self.globals.shortcut_inhibit_manager {
if self.shortcut_inhibitor.is_none() { if self.shortcut_inhibitor.is_none() {
self.shortcut_inhibitor = self.shortcut_inhibitor = Some(shortcut_inhibit_manager.inhibit_shortcuts(
Some(shortcut_inhibit_manager.inhibit_shortcuts(surface, &self.g.seat, qh, ())); surface,
&self.globals.seat,
qh,
(),
));
} }
} }
} }
@@ -469,21 +509,39 @@ 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::debug!("outputs: {outputs:?}"); log::info!(
outputs.iter().for_each(|(o, i)| { "adding capture for position {pos} - using outputs: {:?}",
let window = Window::new(self, &self.qh, o, pos, i.size); outputs
let window = Arc::new(window); .iter()
self.active_windows.push(window); .map(|o| o
.info
.as_ref()
.map(|i| i.name.to_owned())
.unwrap_or("unknown output".to_owned()))
.collect::<Vec<_>>()
);
outputs.iter().for_each(|o| {
if let Some(info) = o.info.as_ref() {
let window = Window::new(self, &self.qh, &o.wl_output, pos, info.size);
let window = Arc::new(window);
self.active_windows.push(window);
}
}); });
} }
fn update_windows(&mut self) { fn update_windows(&mut self) {
log::debug!("updating windows"); log::info!("active outputs: ");
log::debug!("output info: {:?}", self.output_info); for output in self.outputs.iter().filter(|o| o.info.is_some()) {
let clients: Vec<_> = self.active_windows.drain(..).map(|w| w.pos).collect(); log::info!(" * {output}");
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);
} }
} }
@@ -524,17 +582,17 @@ impl Inner {
match self.queue.dispatch_pending(&mut self.state) { match self.queue.dispatch_pending(&mut self.state) {
Ok(_) => {} Ok(_) => {}
Err(DispatchError::Backend(WaylandError::Io(e))) => { Err(DispatchError::Backend(WaylandError::Io(e))) => {
log::error!("Wayland Error: {}", e); log::error!("Wayland Error: {e}");
} }
Err(DispatchError::Backend(e)) => { Err(DispatchError::Backend(e)) => {
panic!("backend error: {}", e); panic!("backend error: {e}");
} }
Err(DispatchError::BadMessage { Err(DispatchError::BadMessage {
sender_id, sender_id,
interface, interface,
opcode, opcode,
}) => { }) => {
panic!("bad message {}, {} , {}", sender_id, interface, opcode); panic!("bad message {sender_id}, {interface} , {opcode}");
} }
} }
} }
@@ -755,7 +813,7 @@ impl Dispatch<WlPointer, ()> for State {
})), })),
)); ));
} }
wl_pointer::Event::Frame {} => { wl_pointer::Event::Frame => {
// TODO properly handle frame events // TODO properly handle frame events
// we simply insert a frame event on the client side // we simply insert a frame event on the client side
// after each event for now // after each event for now
@@ -835,7 +893,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 })),
@@ -872,94 +930,89 @@ impl Dispatch<ZwlrLayerSurfaceV1, ()> for State {
} }
// delegate wl_registry events to App itself // delegate wl_registry events to App itself
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for State { impl Dispatch<WlRegistry, GlobalListContents> for State {
fn event(
_state: &mut Self,
_proxy: &wl_registry::WlRegistry,
_event: <wl_registry::WlRegistry as wayland_client::Proxy>::Event,
_data: &GlobalListContents,
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
}
}
impl Dispatch<wl_registry::WlRegistry, ()> for State {
fn event( fn event(
state: &mut Self, state: &mut Self,
registry: &wl_registry::WlRegistry, _registry: &WlRegistry,
event: <wl_registry::WlRegistry as wayland_client::Proxy>::Event, event: <WlRegistry as wayland_client::Proxy>::Event,
_: &(), _data: &GlobalListContents,
_: &Connection, _conn: &Connection,
qh: &QueueHandle<Self>, _qh: &QueueHandle<Self>,
) { ) {
match event { match event {
wl_registry::Event::Global { wl_registry::Event::Global {
name, name,
interface, interface,
version: _, version,
} => { } => {
if interface.as_str() == "wl_output" { state.register_global(Global {
log::debug!("wl_output global"); name,
state interface,
.g version,
.outputs });
.push(registry.bind::<WlOutput, _, _>(name, 4, qh, ())) }
} wl_registry::Event::GlobalRemove { name } => {
state.deregister_global(name);
} }
wl_registry::Event::GlobalRemove { .. } => {}
_ => {} _ => {}
} }
} }
} }
impl Dispatch<ZxdgOutputV1, WlOutput> for State { impl Dispatch<ZxdgOutputV1, u32> 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,
wl_output: &WlOutput, name: &u32,
_: &Connection, _: &Connection,
_: &QueueHandle<Self>, _: &QueueHandle<Self>,
) { ) {
log::debug!("xdg-output - {event:?}"); let output = state
let output_info = match state.output_info.iter_mut().find(|(o, _)| o == wl_output) { .outputs
Some((_, c)) => c, .iter_mut()
None => { .find(|o| o.global.name == *name)
let output_info = OutputInfo::new(); .expect("output");
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_info.position = (x, y); output.pending_info.position = (x, y);
output.has_xdg_info = true;
} }
zxdg_output_v1::Event::LogicalSize { width, height } => { zxdg_output_v1::Event::LogicalSize { width, height } => {
output_info.size = (width, height); output.pending_info.size = (width, height);
output.has_xdg_info = true;
}
zxdg_output_v1::Event::Done => {
log::warn!("Use of deprecated xdg-output event \"done\"");
state.update_output_info(*name);
} }
zxdg_output_v1::Event::Done => {}
zxdg_output_v1::Event::Name { name } => { zxdg_output_v1::Event::Name { name } => {
output_info.name = name; output.pending_info.name = name;
output.has_xdg_info = true;
} }
zxdg_output_v1::Event::Description { .. } => {} zxdg_output_v1::Event::Description { description } => {
_ => {} output.pending_info.description = description;
output.has_xdg_info = true;
}
_ => todo!(),
} }
} }
} }
impl Dispatch<WlOutput, ()> for State { impl Dispatch<WlOutput, u32> for State {
fn event( fn event(
state: &mut Self, state: &mut Self,
_proxy: &WlOutput, _wl_output: &WlOutput,
event: <WlOutput as wayland_client::Proxy>::Event, event: <WlOutput as wayland_client::Proxy>::Event,
_data: &(), name: &u32,
_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_windows(); state.update_output_info(*name);
} }
} }
} }

View File

@@ -2,14 +2,14 @@ use std::{
collections::{HashMap, HashSet, VecDeque}, collections::{HashMap, HashSet, VecDeque},
fmt::Display, fmt::Display,
mem::swap, mem::swap,
task::{ready, Poll}, task::{Poll, ready},
}; };
use async_trait::async_trait; use async_trait::async_trait;
use futures::StreamExt; use futures::StreamExt;
use futures_core::Stream; use futures_core::Stream;
use input_event::{scancode, Event, KeyboardEvent}; use input_event::{Event, KeyboardEvent, scancode};
pub use error::{CaptureCreationError, CaptureError, InputCaptureError}; pub use error::{CaptureCreationError, CaptureError, InputCaptureError};
@@ -79,7 +79,7 @@ impl Display for Position {
Position::Top => "top", Position::Top => "top",
Position::Bottom => "bottom", Position::Bottom => "bottom",
}; };
write!(f, "{}", pos) write!(f, "{pos}")
} }
} }

View File

@@ -1,10 +1,10 @@
use ashpd::{ use ashpd::{
desktop::{ desktop::{
Session,
input_capture::{ input_capture::{
Activated, ActivatedBarrier, Barrier, BarrierID, Capabilities, InputCapture, Region, Activated, ActivatedBarrier, Barrier, BarrierID, Capabilities, InputCapture, Region,
Zones, Zones,
}, },
Session,
}, },
enumflags2::BitFlags, enumflags2::BitFlags,
}; };
@@ -28,8 +28,8 @@ use std::{
}; };
use tokio::{ use tokio::{
sync::{ sync::{
mpsc::{self, Receiver, Sender},
Notify, Notify,
mpsc::{self, Receiver, Sender},
}, },
task::JoinHandle, task::JoinHandle,
}; };
@@ -42,8 +42,8 @@ use input_event::Event;
use crate::CaptureEvent; use crate::CaptureEvent;
use super::{ use super::{
error::{CaptureError, LibeiCaptureCreationError},
Capture as LanMouseInputCapture, Position, Capture as LanMouseInputCapture, Position,
error::{CaptureError, LibeiCaptureCreationError},
}; };
/* there is a bug in xdg-remote-desktop-portal-gnome / mutter that /* there is a bug in xdg-remote-desktop-portal-gnome / mutter that
@@ -587,9 +587,13 @@ impl LanMouseInputCapture for LibeiInputCapture<'_> {
self.cancellation_token.cancel(); self.cancellation_token.cancel();
let task = &mut self.capture_task; let task = &mut self.capture_task;
log::debug!("waiting for capture to terminate..."); log::debug!("waiting for capture to terminate...");
let res = task.await.expect("libei task panic"); let res = if !task.is_finished() {
log::debug!("done!"); task.await.expect("libei task panic")
} else {
Ok(())
};
self.terminated = true; self.terminated = true;
log::debug!("done!");
res res
} }
} }

View File

@@ -1,31 +1,40 @@
use super::{error::MacosCaptureCreationError, Capture, CaptureError, CaptureEvent, Position}; use super::{Capture, CaptureError, CaptureEvent, Position, error::MacosCaptureCreationError};
use async_trait::async_trait; use async_trait::async_trait;
use bitflags::bitflags; use bitflags::bitflags;
use core_foundation::base::{kCFAllocatorDefault, CFRelease}; use core_foundation::{
use core_foundation::date::CFTimeInterval; base::{CFRelease, kCFAllocatorDefault},
use core_foundation::number::{kCFBooleanTrue, CFBooleanRef}; date::CFTimeInterval,
use core_foundation::runloop::{kCFRunLoopCommonModes, CFRunLoop, CFRunLoopSource}; number::{CFBooleanRef, kCFBooleanTrue},
use core_foundation::string::{kCFStringEncodingUTF8, CFStringCreateWithCString, CFStringRef}; runloop::{CFRunLoop, CFRunLoopSource, kCFRunLoopCommonModes},
use core_graphics::base::{kCGErrorSuccess, CGError}; string::{CFStringCreateWithCString, CFStringRef, kCFStringEncodingUTF8},
use core_graphics::display::{CGDisplay, CGPoint}; };
use core_graphics::event::{ use core_graphics::{
CGEvent, CGEventFlags, CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement, base::{CGError, kCGErrorSuccess},
CGEventTapProxy, CGEventType, EventField, display::{CGDisplay, CGPoint},
event::{
CGEvent, CGEventFlags, CGEventTap, CGEventTapLocation, CGEventTapOptions,
CGEventTapPlacement, CGEventTapProxy, CGEventType, CallbackResult, EventField,
},
event_source::{CGEventSource, CGEventSourceStateID},
}; };
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
use futures_core::Stream; use futures_core::Stream;
use input_event::{Event, KeyboardEvent, PointerEvent, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT}; use input_event::{BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, Event, KeyboardEvent, PointerEvent};
use keycode::{KeyMap, KeyMapping}; use keycode::{KeyMap, KeyMapping};
use libc::c_void; use libc::c_void;
use once_cell::unsync::Lazy; use once_cell::unsync::Lazy;
use std::collections::HashSet; use std::{
use std::ffi::{c_char, CString}; collections::HashSet,
use std::pin::Pin; ffi::{CString, c_char},
use std::sync::Arc; pin::Pin,
use std::task::{ready, Context, Poll}; sync::Arc,
use std::thread::{self}; task::{Context, Poll, ready},
use tokio::sync::mpsc::{self, Receiver, Sender}; thread::{self},
use tokio::sync::{oneshot, Mutex}; };
use tokio::sync::{
Mutex,
mpsc::{self, Receiver, Sender},
oneshot,
};
#[derive(Debug, Default)] #[derive(Debug, Default)]
struct Bounds { struct Bounds {
@@ -37,9 +46,16 @@ struct Bounds {
#[derive(Debug)] #[derive(Debug)]
struct InputCaptureState { struct InputCaptureState {
/// active capture positions
active_clients: Lazy<HashSet<Position>>, active_clients: Lazy<HashSet<Position>>,
/// the currently entered capture position, if any
current_pos: Option<Position>, current_pos: Option<Position>,
/// position where the cursor was captured
enter_position: Option<CGPoint>,
/// bounds of the input capture area
bounds: Bounds, bounds: Bounds,
/// current state of modifier keys
modifier_state: XMods,
} }
#[derive(Debug)] #[derive(Debug)]
@@ -56,7 +72,9 @@ impl InputCaptureState {
let mut res = Self { let mut res = Self {
active_clients: Lazy::new(HashSet::new), active_clients: Lazy::new(HashSet::new),
current_pos: None, current_pos: None,
enter_position: None,
bounds: Bounds::default(), bounds: Bounds::default(),
modifier_state: Default::default(),
}; };
res.update_bounds()?; res.update_bounds()?;
Ok(res) Ok(res)
@@ -96,45 +114,34 @@ impl InputCaptureState {
Ok(()) Ok(())
} }
// We can't disable mouse movement when in a client so we need to reset the cursor position /// start the input capture by
// to the edge of the screen, the cursor will be hidden but we dont want it to appear in a fn start_capture(&mut self, event: &CGEvent, position: Position) -> Result<(), CaptureError> {
// random location when we exit the client let mut location = event.location();
fn reset_mouse_position(&self, event: &CGEvent) -> Result<(), CaptureError> { let edge_offset = 1.0;
if let Some(pos) = self.current_pos { // move cursor location to display bounds
let location = event.location(); match position {
let edge_offset = 1.0; Position::Left => location.x = self.bounds.xmin + edge_offset,
Position::Right => location.x = self.bounds.xmax - edge_offset,
Position::Top => location.y = self.bounds.ymin + edge_offset,
Position::Bottom => location.y = self.bounds.ymax - edge_offset,
};
self.enter_position = Some(location);
self.reset_cursor()
}
// After the cursor is warped no event is produced but the next event /// resets the cursor to the position, where the capture started
// will carry the delta from the warp so only half the delta is needed to move the cursor fn reset_cursor(&mut self) -> Result<(), CaptureError> {
let delta_y = event.get_double_value_field(EventField::MOUSE_EVENT_DELTA_Y) / 2.0; let pos = self.enter_position.expect("capture active");
let delta_x = event.get_double_value_field(EventField::MOUSE_EVENT_DELTA_X) / 2.0; log::trace!("Resetting cursor position to: {}, {}", pos.x, pos.y);
CGDisplay::warp_mouse_cursor_position(pos).map_err(CaptureError::WarpCursor)
}
let mut new_x = location.x + delta_x; fn hide_cursor(&self) -> Result<(), CaptureError> {
let mut new_y = location.y + delta_y; CGDisplay::hide_cursor(&CGDisplay::main()).map_err(CaptureError::CoreGraphics)
}
match pos { fn show_cursor(&self) -> Result<(), CaptureError> {
Position::Left => { CGDisplay::show_cursor(&CGDisplay::main()).map_err(CaptureError::CoreGraphics)
new_x = self.bounds.xmin + edge_offset;
}
Position::Right => {
new_x = self.bounds.xmax - edge_offset;
}
Position::Top => {
new_y = self.bounds.ymin + edge_offset;
}
Position::Bottom => {
new_y = self.bounds.ymax - edge_offset;
}
}
let new_pos = CGPoint::new(new_x, new_y);
log::trace!("Resetting cursor position to: {new_x}, {new_y}");
return CGDisplay::warp_mouse_cursor_position(new_pos)
.map_err(CaptureError::WarpCursor);
}
Err(CaptureError::ResetMouseWithoutClient)
} }
async fn handle_producer_event( async fn handle_producer_event(
@@ -145,15 +152,13 @@ impl InputCaptureState {
match producer_event { match producer_event {
ProducerEvent::Release => { ProducerEvent::Release => {
if self.current_pos.is_some() { if self.current_pos.is_some() {
CGDisplay::show_cursor(&CGDisplay::main()) self.show_cursor()?;
.map_err(CaptureError::CoreGraphics)?;
self.current_pos = None; self.current_pos = None;
} }
} }
ProducerEvent::Grab(pos) => { ProducerEvent::Grab(pos) => {
if self.current_pos.is_none() { if self.current_pos.is_none() {
CGDisplay::hide_cursor(&CGDisplay::main()) self.hide_cursor()?;
.map_err(CaptureError::CoreGraphics)?;
self.current_pos = Some(pos); self.current_pos = Some(pos);
} }
} }
@@ -163,8 +168,7 @@ impl InputCaptureState {
ProducerEvent::Destroy(p) => { ProducerEvent::Destroy(p) => {
if let Some(current) = self.current_pos { if let Some(current) = self.current_pos {
if current == p { if current == p {
CGDisplay::show_cursor(&CGDisplay::main()) self.show_cursor()?;
.map_err(CaptureError::CoreGraphics)?;
self.current_pos = None; self.current_pos = None;
}; };
} }
@@ -180,6 +184,7 @@ fn get_events(
ev_type: &CGEventType, ev_type: &CGEventType,
ev: &CGEvent, ev: &CGEvent,
result: &mut Vec<CaptureEvent>, result: &mut Vec<CaptureEvent>,
modifier_state: &mut XMods,
) -> Result<(), CaptureError> { ) -> Result<(), CaptureError> {
fn map_pointer_event(ev: &CGEvent) -> PointerEvent { fn map_pointer_event(ev: &CGEvent) -> PointerEvent {
PointerEvent::Motion { PointerEvent::Motion {
@@ -215,29 +220,42 @@ fn get_events(
}))); })));
} }
CGEventType::FlagsChanged => { CGEventType::FlagsChanged => {
let mut mods = XMods::empty(); let mut depressed = XMods::empty();
let mut mods_locked = XMods::empty(); let mut mods_locked = XMods::empty();
let cg_flags = ev.get_flags(); let cg_flags = ev.get_flags();
if cg_flags.contains(CGEventFlags::CGEventFlagShift) { if cg_flags.contains(CGEventFlags::CGEventFlagShift) {
mods |= XMods::ShiftMask; depressed |= XMods::ShiftMask;
} }
if cg_flags.contains(CGEventFlags::CGEventFlagControl) { if cg_flags.contains(CGEventFlags::CGEventFlagControl) {
mods |= XMods::ControlMask; depressed |= XMods::ControlMask;
} }
if cg_flags.contains(CGEventFlags::CGEventFlagAlternate) { if cg_flags.contains(CGEventFlags::CGEventFlagAlternate) {
mods |= XMods::Mod1Mask; depressed |= XMods::Mod1Mask;
} }
if cg_flags.contains(CGEventFlags::CGEventFlagCommand) { if cg_flags.contains(CGEventFlags::CGEventFlagCommand) {
mods |= XMods::Mod4Mask; depressed |= XMods::Mod4Mask;
} }
if cg_flags.contains(CGEventFlags::CGEventFlagAlphaShift) { if cg_flags.contains(CGEventFlags::CGEventFlagAlphaShift) {
mods |= XMods::LockMask; depressed |= XMods::LockMask;
mods_locked |= XMods::LockMask; mods_locked |= XMods::LockMask;
} }
// check if pressed or released
let state = if depressed > *modifier_state { 1 } else { 0 };
*modifier_state = depressed;
if let Ok(key) = map_key(ev) {
let key_event = CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Key {
time: 0,
key,
state,
}));
result.push(key_event);
}
let modifier_event = KeyboardEvent::Modifiers { let modifier_event = KeyboardEvent::Modifiers {
depressed: mods.bits(), depressed: depressed.bits(),
latched: 0, latched: 0,
locked: mods_locked.bits(), locked: mods_locked.bits(),
group: 0, group: 0,
@@ -300,21 +318,47 @@ fn get_events(
}))) })))
} }
CGEventType::ScrollWheel => { CGEventType::ScrollWheel => {
let v = ev.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_POINT_DELTA_AXIS_1); if ev.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_IS_CONTINUOUS) != 0 {
let h = ev.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_POINT_DELTA_AXIS_2); let v =
if v != 0 { ev.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_POINT_DELTA_AXIS_1);
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Axis { let h =
time: 0, ev.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_POINT_DELTA_AXIS_2);
axis: 0, // Vertical if v != 0 {
value: v as f64, result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Axis {
}))); time: 0,
} axis: 0, // Vertical
if h != 0 { value: v as f64,
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Axis { })));
time: 0, }
axis: 1, // Horizontal if h != 0 {
value: h as f64, result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Axis {
}))); time: 0,
axis: 1, // Horizontal
value: h as f64,
})));
}
} else {
// line based scrolling
const LINES_PER_STEP: i32 = 3;
const V120_STEPS_PER_LINE: i32 = 120 / LINES_PER_STEP;
let v = ev.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_DELTA_AXIS_1);
let h = ev.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_DELTA_AXIS_2);
if v != 0 {
result.push(CaptureEvent::Input(Event::Pointer(
PointerEvent::AxisDiscrete120 {
axis: 0, // Vertical
value: V120_STEPS_PER_LINE * v as i32,
},
)));
}
if h != 0 {
result.push(CaptureEvent::Input(Event::Pointer(
PointerEvent::AxisDiscrete120 {
axis: 1, // Horizontal
value: V120_STEPS_PER_LINE * h as i32,
},
)));
}
} }
} }
_ => (), _ => (),
@@ -348,7 +392,7 @@ fn create_event_tap<'a>(
move |_proxy: CGEventTapProxy, event_type: CGEventType, cg_ev: &CGEvent| { move |_proxy: CGEventTapProxy, event_type: CGEventType, cg_ev: &CGEvent| {
log::trace!("Got event from tap: {event_type:?}"); log::trace!("Got event from tap: {event_type:?}");
let mut state = client_state.blocking_lock(); let mut state = client_state.blocking_lock();
let mut pos = None; let mut capture_position = None;
let mut res_events = vec![]; let mut res_events = vec![];
if matches!( if matches!(
@@ -365,22 +409,34 @@ fn create_event_tap<'a>(
// Are we in a client? // Are we in a client?
if let Some(current_pos) = state.current_pos { if let Some(current_pos) = state.current_pos {
pos = Some(current_pos); capture_position = Some(current_pos);
get_events(&event_type, cg_ev, &mut res_events).unwrap_or_else(|e| { get_events(
&event_type,
cg_ev,
&mut res_events,
&mut state.modifier_state,
)
.unwrap_or_else(|e| {
log::error!("Failed to get events: {e}"); log::error!("Failed to get events: {e}");
}); });
// Keep (hidden) cursor at the edge of the screen // Keep (hidden) cursor at the edge of the screen
if matches!(event_type, CGEventType::MouseMoved) { if matches!(
state.reset_mouse_position(cg_ev).unwrap_or_else(|e| { event_type,
log::error!("Failed to reset mouse position: {e}"); CGEventType::MouseMoved
}) | CGEventType::LeftMouseDragged
| CGEventType::RightMouseDragged
| CGEventType::OtherMouseDragged
) {
state.reset_cursor().unwrap_or_else(|e| log::warn!("{e}"));
} }
} } else if matches!(event_type, CGEventType::MouseMoved) {
// Did we cross a barrier? // Did we cross a barrier?
else if matches!(event_type, CGEventType::MouseMoved) {
if let Some(new_pos) = state.crossed(cg_ev) { if let Some(new_pos) = state.crossed(cg_ev) {
pos = Some(new_pos); capture_position = Some(new_pos);
state
.start_capture(cg_ev, new_pos)
.unwrap_or_else(|e| log::warn!("{e}"));
res_events.push(CaptureEvent::Begin); res_events.push(CaptureEvent::Begin);
notify_tx notify_tx
.blocking_send(ProducerEvent::Grab(new_pos)) .blocking_send(ProducerEvent::Grab(new_pos))
@@ -388,17 +444,17 @@ fn create_event_tap<'a>(
} }
} }
if let Some(pos) = pos { if let Some(pos) = capture_position {
res_events.iter().for_each(|e| { res_events.iter().for_each(|e| {
event_tx // error must be ignored, since the event channel
.blocking_send((pos, *e)) // may already be closed when the InputCapture instance is dropped.
.expect("Failed to send event"); let _ = event_tx.blocking_send((pos, *e));
}); });
// Returning None should stop the event from being processed // Returning Drop should stop the event from being processed
// but core fundation still returns the event // but core fundation still returns the event
cg_ev.set_type(CGEventType::Null); cg_ev.set_type(CGEventType::Null);
} }
Some(cg_ev.to_owned()) CallbackResult::Replace(cg_ev.to_owned())
}; };
let tap = CGEventTap::new( let tap = CGEventTap::new(
@@ -411,7 +467,7 @@ fn create_event_tap<'a>(
.map_err(|_| MacosCaptureCreationError::EventTapCreation)?; .map_err(|_| MacosCaptureCreationError::EventTapCreation)?;
let tap_source: CFRunLoopSource = tap let tap_source: CFRunLoopSource = tap
.mach_port .mach_port()
.create_runloop_source(0) .create_runloop_source(0)
.expect("Failed creating loop source"); .expect("Failed creating loop source");
@@ -426,8 +482,8 @@ fn event_tap_thread(
client_state: Arc<Mutex<InputCaptureState>>, client_state: Arc<Mutex<InputCaptureState>>,
event_tx: Sender<(Position, CaptureEvent)>, event_tx: Sender<(Position, CaptureEvent)>,
notify_tx: Sender<ProducerEvent>, notify_tx: Sender<ProducerEvent>,
ready: std::sync::mpsc::Sender<Result<(), MacosCaptureCreationError>>, ready: std::sync::mpsc::Sender<Result<CFRunLoop, MacosCaptureCreationError>>,
exit: oneshot::Sender<Result<(), &'static str>>, exit: oneshot::Sender<()>,
) { ) {
let _tap = match create_event_tap(client_state, notify_tx, event_tx) { let _tap = match create_event_tap(client_state, notify_tx, event_tx) {
Err(e) => { Err(e) => {
@@ -435,18 +491,22 @@ fn event_tap_thread(
return; return;
} }
Ok(tap) => { Ok(tap) => {
ready.send(Ok(())).expect("channel closed"); let run_loop = CFRunLoop::get_current();
ready.send(Ok(run_loop)).expect("channel closed");
tap tap
} }
}; };
log::debug!("running CFRunLoop...");
CFRunLoop::run_current(); CFRunLoop::run_current();
log::debug!("event tap thread exiting!...");
let _ = exit.send(Err("tap thread exited")); let _ = exit.send(());
} }
pub struct MacOSInputCapture { pub struct MacOSInputCapture {
event_rx: Receiver<(Position, CaptureEvent)>, event_rx: Receiver<(Position, CaptureEvent)>,
notify_tx: Sender<ProducerEvent>, notify_tx: Sender<ProducerEvent>,
run_loop: CFRunLoop,
} }
impl MacOSInputCapture { impl MacOSInputCapture {
@@ -475,36 +535,41 @@ impl MacOSInputCapture {
}); });
// wait for event tap creation result // wait for event tap creation result
ready_rx.recv().expect("channel closed")?; let run_loop = ready_rx.recv().expect("channel closed")?;
let _tap_task: tokio::task::JoinHandle<()> = tokio::task::spawn_local(async move { let _tap_task: tokio::task::JoinHandle<()> = tokio::task::spawn_local(async move {
loop { loop {
tokio::select! { tokio::select! {
producer_event = notify_rx.recv() => { producer_event = notify_rx.recv() => {
let producer_event = producer_event.expect("channel closed"); let Some(producer_event) = producer_event else {
break;
};
let mut state = state.lock().await; let mut state = state.lock().await;
state.handle_producer_event(producer_event).await.unwrap_or_else(|e| { state.handle_producer_event(producer_event).await.unwrap_or_else(|e| {
log::error!("Failed to handle producer event: {e}"); log::error!("Failed to handle producer event: {e}");
}) })
} }
_ = &mut tap_exit_rx => break,
res = &mut tap_exit_rx => {
if let Err(e) = res.expect("channel closed") {
log::error!("Tap thread failed: {:?}", e);
break;
}
}
} }
} }
// show cursor
let _ = CGDisplay::show_cursor(&CGDisplay::main());
}); });
Ok(Self { Ok(Self {
event_rx, event_rx,
notify_tx, notify_tx,
run_loop,
}) })
} }
} }
impl Drop for MacOSInputCapture {
fn drop(&mut self) {
self.run_loop.stop();
}
}
#[async_trait] #[async_trait]
impl Capture for MacOSInputCapture { impl Capture for MacOSInputCapture {
async fn create(&mut self, pos: Position) -> Result<(), CaptureError> { async fn create(&mut self, pos: Position) -> Result<(), CaptureError> {

View File

@@ -5,7 +5,7 @@ use futures::Stream;
use std::pin::Pin; use std::pin::Pin;
use std::task::ready; use std::task::ready;
use tokio::sync::mpsc::{channel, Receiver}; use tokio::sync::mpsc::{Receiver, channel};
use super::{Capture, CaptureError, CaptureEvent, Position}; use super::{Capture, CaptureError, CaptureEvent, Position};

View File

@@ -6,33 +6,32 @@ use std::default::Default;
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
use std::sync::{Arc, Condvar, Mutex}; use std::sync::{Arc, Condvar, Mutex};
use std::thread; use std::thread;
use tokio::sync::mpsc::error::TrySendError;
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use windows::core::{w, PCWSTR}; use tokio::sync::mpsc::error::TrySendError;
use windows::Win32::Foundation::{FALSE, HINSTANCE, HWND, LPARAM, LRESULT, RECT, WPARAM}; use windows::Win32::Foundation::{FALSE, HWND, LPARAM, LRESULT, RECT, WPARAM};
use windows::Win32::Graphics::Gdi::{ use windows::Win32::Graphics::Gdi::{
EnumDisplayDevicesW, EnumDisplaySettingsW, DEVMODEW, DISPLAY_DEVICEW, DEVMODEW, DISPLAY_DEVICE_ATTACHED_TO_DESKTOP, DISPLAY_DEVICEW, ENUM_CURRENT_SETTINGS,
DISPLAY_DEVICE_ATTACHED_TO_DESKTOP, ENUM_CURRENT_SETTINGS, EnumDisplayDevicesW, EnumDisplaySettingsW,
}; };
use windows::Win32::System::LibraryLoader::GetModuleHandleW; use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::System::Threading::GetCurrentThreadId; use windows::Win32::System::Threading::GetCurrentThreadId;
use windows::core::{PCWSTR, w};
use windows::Win32::UI::WindowsAndMessaging::{ use windows::Win32::UI::WindowsAndMessaging::{
CallNextHookEx, CreateWindowExW, DispatchMessageW, GetMessageW, PostThreadMessageW, CallNextHookEx, CreateWindowExW, DispatchMessageW, EDD_GET_DEVICE_INTERFACE_NAME, GetMessageW,
RegisterClassW, SetWindowsHookExW, TranslateMessage, EDD_GET_DEVICE_INTERFACE_NAME, HHOOK, HOOKPROC, KBDLLHOOKSTRUCT, LLKHF_EXTENDED, MSG, MSLLHOOKSTRUCT, PostThreadMessageW,
HMENU, HOOKPROC, KBDLLHOOKSTRUCT, LLKHF_EXTENDED, MSG, MSLLHOOKSTRUCT, WH_KEYBOARD_LL, RegisterClassW, SetWindowsHookExW, TranslateMessage, WH_KEYBOARD_LL, WH_MOUSE_LL, WINDOW_STYLE,
WH_MOUSE_LL, WINDOW_STYLE, WM_DISPLAYCHANGE, WM_KEYDOWN, WM_KEYUP, WM_LBUTTONDOWN, WM_DISPLAYCHANGE, WM_KEYDOWN, WM_KEYUP, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN,
WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_RBUTTONDOWN, WM_MBUTTONUP, WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_RBUTTONDOWN, WM_RBUTTONUP,
WM_RBUTTONUP, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_USER, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSW, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_USER, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSW, WNDPROC,
WNDPROC,
}; };
use input_event::{ use input_event::{
BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, Event, KeyboardEvent, PointerEvent,
scancode::{self, Linux}, scancode::{self, Linux},
Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT,
}; };
use super::{display_util, CaptureEvent, Position}; use super::{CaptureEvent, Position, display_util};
pub(crate) struct EventThread { pub(crate) struct EventThread {
request_buffer: Arc<Mutex<Vec<ClientUpdate>>>, request_buffer: Arc<Mutex<Vec<ClientUpdate>>>,
@@ -128,7 +127,7 @@ thread_local! {
fn get_msg() -> Option<MSG> { fn get_msg() -> Option<MSG> {
unsafe { unsafe {
let mut msg = std::mem::zeroed(); let mut msg = std::mem::zeroed();
let ret = GetMessageW(addr_of_mut!(msg), HWND::default(), 0, 0); let ret = GetMessageW(addr_of_mut!(msg), None, 0, 0);
match ret.0 { match ret.0 {
0 => None, 0 => None,
x if x > 0 => Some(msg), x if x > 0 => Some(msg),
@@ -176,14 +175,15 @@ fn start_routine(
/* register hooks */ /* register hooks */
unsafe { unsafe {
let _ = SetWindowsHookExW(WH_MOUSE_LL, mouse_proc, HINSTANCE::default(), 0).unwrap(); let _ = SetWindowsHookExW(WH_MOUSE_LL, mouse_proc, None, 0).unwrap();
let _ = SetWindowsHookExW(WH_KEYBOARD_LL, kybrd_proc, HINSTANCE::default(), 0).unwrap(); let _ = SetWindowsHookExW(WH_KEYBOARD_LL, kybrd_proc, None, 0).unwrap();
} }
let instance = unsafe { GetModuleHandleW(None).unwrap() }; let instance = unsafe { GetModuleHandleW(None).unwrap() };
let instance = instance.into();
let window_class: WNDCLASSW = WNDCLASSW { let window_class: WNDCLASSW = WNDCLASSW {
lpfnWndProc: window_proc, lpfnWndProc: window_proc,
hInstance: instance.into(), hInstance: instance,
lpszClassName: w!("lan-mouse-message-window-class"), lpszClassName: w!("lan-mouse-message-window-class"),
..Default::default() ..Default::default()
}; };
@@ -213,9 +213,9 @@ fn start_routine(
0, 0,
0, 0,
0, 0,
HWND::default(), None,
HMENU::default(), None,
instance, Some(instance),
None, None,
) )
.expect("CreateWindowExW"); .expect("CreateWindowExW");
@@ -312,7 +312,7 @@ unsafe extern "system" fn mouse_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM)
/* no client was active */ /* no client was active */
if !active { if !active {
return CallNextHookEx(HHOOK::default(), ncode, wparam, lparam); return CallNextHookEx(None, ncode, wparam, lparam);
} }
/* get active client if any */ /* get active client if any */
@@ -337,7 +337,7 @@ unsafe extern "system" fn mouse_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM)
unsafe extern "system" fn kybrd_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT { unsafe extern "system" fn kybrd_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
/* get active client if any */ /* get active client if any */
let Some(client) = ACTIVE_CLIENT.get() else { let Some(client) = ACTIVE_CLIENT.get() else {
return CallNextHookEx(HHOOK::default(), ncode, wparam, lparam); return CallNextHookEx(None, ncode, wparam, lparam);
}; };
/* convert to key event */ /* convert to key event */
@@ -388,7 +388,10 @@ fn enumerate_displays(display_rects: &mut Vec<RECT>) {
if ret == FALSE { if ret == FALSE {
break; break;
} }
if device.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP != 0 { if device
.StateFlags
.contains(DISPLAY_DEVICE_ATTACHED_TO_DESKTOP)
{
devices.push(device.DeviceName); devices.push(device.DeviceName);
} }
} }
@@ -537,6 +540,10 @@ fn to_mouse_event(wparam: WPARAM, lparam: LPARAM) -> Option<PointerEvent> {
state: if p == WM_XBUTTONDOWN as usize { 1 } else { 0 }, state: if p == WM_XBUTTONDOWN as usize { 1 } else { 0 },
}) })
} }
WPARAM(p) if p == WM_MOUSEHWHEEL as usize => Some(PointerEvent::AxisDiscrete120 {
axis: 1, // Horizontal
value: mouse_low_level.mouseData as i32 >> 16,
}),
w => { w => {
log::warn!("unknown mouse event: {w:?}"); log::warn!("unknown mouse event: {w:?}");
None None

View File

@@ -3,7 +3,7 @@ use std::task::Poll;
use async_trait::async_trait; use async_trait::async_trait;
use futures_core::Stream; use futures_core::Stream;
use super::{error::X11InputCaptureCreationError, Capture, CaptureError, CaptureEvent, Position}; use super::{Capture, CaptureError, CaptureEvent, Position, error::X11InputCaptureCreationError};
pub struct X11InputCapture {} pub struct X11InputCapture {}

View File

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

View File

@@ -11,15 +11,15 @@ pub enum InputEmulationError {
any(feature = "remote_desktop_portal", feature = "libei"), any(feature = "remote_desktop_portal", feature = "libei"),
not(target_os = "macos") not(target_os = "macos")
))] ))]
use ashpd::{desktop::ResponseError, Error::Response}; use ashpd::{Error::Response, desktop::ResponseError};
use std::io; use std::io;
use thiserror::Error; use thiserror::Error;
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))] #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
use wayland_client::{ use wayland_client::{
ConnectError, DispatchError,
backend::WaylandError, backend::WaylandError,
globals::{BindError, GlobalError}, globals::{BindError, GlobalError},
ConnectError, DispatchError,
}; };
#[derive(Debug, Error)] #[derive(Debug, Error)]

View File

@@ -1,25 +1,25 @@
use futures::{future, StreamExt}; use futures::{StreamExt, future};
use std::{ use std::{
io, io,
os::{fd::OwnedFd, unix::net::UnixStream}, os::{fd::OwnedFd, unix::net::UnixStream},
sync::{ sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex, RwLock, Arc, Mutex, RwLock,
atomic::{AtomicBool, Ordering},
}, },
time::{SystemTime, UNIX_EPOCH}, time::{SystemTime, UNIX_EPOCH},
}; };
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use ashpd::desktop::{ use ashpd::desktop::{
remote_desktop::{DeviceType, RemoteDesktop},
PersistMode, Session, PersistMode, Session,
remote_desktop::{DeviceType, RemoteDesktop},
}; };
use async_trait::async_trait; use async_trait::async_trait;
use reis::{ use reis::{
ei::{ ei::{
self, button::ButtonState, handshake::ContextType, keyboard::KeyState, Button, Keyboard, self, Button, Keyboard, Pointer, Scroll, button::ButtonState, handshake::ContextType,
Pointer, Scroll, keyboard::KeyState,
}, },
event::{self, Connection, DeviceCapability, DeviceEvent, EiEvent, SeatEvent}, event::{self, Connection, DeviceCapability, DeviceEvent, EiEvent, SeatEvent},
tokio::EiConvertEventStream, tokio::EiConvertEventStream,
@@ -29,7 +29,7 @@ use input_event::{Event, KeyboardEvent, PointerEvent};
use crate::error::EmulationError; use crate::error::EmulationError;
use super::{error::LibeiEmulationCreationError, Emulation, EmulationHandle}; use super::{Emulation, EmulationHandle, error::LibeiEmulationCreationError};
#[derive(Clone, Default)] #[derive(Clone, Default)]
struct Devices { struct Devices {
@@ -50,8 +50,8 @@ pub(crate) struct LibeiEmulation<'a> {
session: Session<'a, RemoteDesktop<'a>>, session: Session<'a, RemoteDesktop<'a>>,
} }
async fn get_ei_fd<'a>( async fn get_ei_fd<'a>()
) -> Result<(RemoteDesktop<'a>, Session<'a, RemoteDesktop<'a>>, OwnedFd), ashpd::Error> { -> Result<(RemoteDesktop<'a>, Session<'a, RemoteDesktop<'a>>, OwnedFd), ashpd::Error> {
let remote_desktop = RemoteDesktop::new().await?; let remote_desktop = RemoteDesktop::new().await?;
log::debug!("creating session ..."); log::debug!("creating session ...");

View File

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

View File

@@ -1,22 +1,22 @@
use super::error::{EmulationError, WindowsEmulationCreationError}; use super::error::{EmulationError, WindowsEmulationCreationError};
use input_event::{ use input_event::{
scancode, Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, Event, KeyboardEvent, PointerEvent,
BTN_RIGHT, scancode,
}; };
use async_trait::async_trait; use async_trait::async_trait;
use std::ops::BitOrAssign; use std::ops::BitOrAssign;
use std::time::Duration; use std::time::Duration;
use tokio::task::AbortHandle; use tokio::task::AbortHandle;
use windows::Win32::UI::Input::KeyboardAndMouse::{
SendInput, INPUT_0, KEYEVENTF_EXTENDEDKEY, MOUSEEVENTF_XDOWN, MOUSEEVENTF_XUP,
};
use windows::Win32::UI::Input::KeyboardAndMouse::{ use windows::Win32::UI::Input::KeyboardAndMouse::{
INPUT, INPUT_KEYBOARD, INPUT_MOUSE, KEYBDINPUT, KEYEVENTF_KEYUP, KEYEVENTF_SCANCODE, INPUT, INPUT_KEYBOARD, INPUT_MOUSE, KEYBDINPUT, KEYEVENTF_KEYUP, KEYEVENTF_SCANCODE,
MOUSEEVENTF_HWHEEL, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_HWHEEL, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MIDDLEDOWN,
MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP, MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP,
MOUSEEVENTF_WHEEL, MOUSEINPUT, MOUSEEVENTF_WHEEL, MOUSEINPUT,
}; };
use windows::Win32::UI::Input::KeyboardAndMouse::{
INPUT_0, KEYEVENTF_EXTENDEDKEY, MOUSEEVENTF_XDOWN, MOUSEEVENTF_XUP, SendInput,
};
use windows::Win32::UI::WindowsAndMessaging::{XBUTTON1, XBUTTON2}; use windows::Win32::UI::WindowsAndMessaging::{XBUTTON1, XBUTTON2};
use super::{Emulation, EmulationHandle}; use super::{Emulation, EmulationHandle};

View File

@@ -1,6 +1,6 @@
use crate::error::EmulationError; use crate::error::EmulationError;
use super::{error::WlrootsEmulationCreationError, Emulation}; use super::{Emulation, error::WlrootsEmulationCreationError};
use async_trait::async_trait; use async_trait::async_trait;
use bitflags::bitflags; use bitflags::bitflags;
use std::collections::HashMap; use std::collections::HashMap;
@@ -8,11 +8,11 @@ use std::io;
use std::os::fd::{AsFd, OwnedFd}; use std::os::fd::{AsFd, OwnedFd};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use wayland_client::backend::WaylandError;
use wayland_client::WEnum; use wayland_client::WEnum;
use wayland_client::backend::WaylandError;
use wayland_client::protocol::wl_keyboard::{self, WlKeyboard}; use wayland_client::protocol::wl_keyboard::{self, WlKeyboard};
use wayland_client::protocol::wl_pointer::{Axis, ButtonState}; use wayland_client::protocol::wl_pointer::{Axis, AxisSource, ButtonState};
use wayland_client::protocol::wl_seat::WlSeat; use wayland_client::protocol::wl_seat::WlSeat;
use wayland_protocols_wlr::virtual_pointer::v1::client::{ use wayland_protocols_wlr::virtual_pointer::v1::client::{
zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1 as VpManager, zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1 as VpManager,
@@ -25,16 +25,15 @@ use wayland_protocols_misc::zwp_virtual_keyboard_v1::client::{
}; };
use wayland_client::{ use wayland_client::{
delegate_noop, Connection, Dispatch, EventQueue, QueueHandle, delegate_noop,
globals::{registry_queue_init, GlobalListContents}, globals::{GlobalListContents, registry_queue_init},
protocol::{wl_registry, wl_seat}, protocol::{wl_registry, wl_seat},
Connection, Dispatch, EventQueue, QueueHandle,
}; };
use input_event::{scancode, Event, KeyboardEvent, PointerEvent}; use input_event::{Event, KeyboardEvent, PointerEvent, scancode};
use super::error::WaylandBindError;
use super::EmulationHandle; use super::EmulationHandle;
use super::error::WaylandBindError;
struct State { struct State {
keymap: Option<(u32, OwnedFd, u32)>, keymap: Option<(u32, OwnedFd, u32)>,
@@ -163,13 +162,13 @@ impl Emulation for WlrootsEmulation {
async fn create(&mut self, handle: EmulationHandle) { async fn create(&mut self, handle: EmulationHandle) {
self.state.add_client(handle); self.state.add_client(handle);
if let Err(e) = self.queue.flush() { if let Err(e) = self.queue.flush() {
log::error!("{}", e); log::error!("{e}");
} }
} }
async fn destroy(&mut self, handle: EmulationHandle) { async fn destroy(&mut self, handle: EmulationHandle) {
self.state.destroy_client(handle); self.state.destroy_client(handle);
if let Err(e) = self.queue.flush() { if let Err(e) = self.queue.flush() {
log::error!("{}", e); log::error!("{e}");
} }
} }
async fn terminate(&mut self) { async fn terminate(&mut self) {
@@ -210,7 +209,8 @@ 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(now, axis, value as f64 / 8., value / 120);
self.pointer.axis_source(AxisSource::Wheel);
self.pointer.frame(); self.pointer.frame();
} }
} }
@@ -221,7 +221,7 @@ impl VirtualInput {
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 let Ok(mut mods) = self.modifiers.lock() {
if mods.update_by_key_event(key, state) { if mods.update_by_key_event(key, state) {
log::trace!("Key triggers modifier change: {:?}", mods); log::trace!("Key triggers modifier change: {mods:?}");
self.keyboard.modifiers( self.keyboard.modifiers(
mods.mask_pressed().bits(), mods.mask_pressed().bits(),
0, 0,
@@ -330,7 +330,7 @@ impl XMods {
fn update_by_key_event(&mut self, key: u32, state: u8) -> bool { fn update_by_key_event(&mut self, key: u32, state: u8) -> bool {
if let Ok(key) = scancode::Linux::try_from(key) { if let Ok(key) = scancode::Linux::try_from(key) {
log::trace!("Attempting to process modifier from: {:#?}", key); log::trace!("Attempting to process modifier from: {key:#?}");
let pressed_mask = match key { let pressed_mask = match key {
scancode::Linux::KeyLeftShift | scancode::Linux::KeyRightShift => XMods::ShiftMask, scancode::Linux::KeyLeftShift | scancode::Linux::KeyRightShift => XMods::ShiftMask,
scancode::Linux::KeyLeftCtrl | scancode::Linux::KeyRightCtrl => XMods::ControlMask, scancode::Linux::KeyLeftCtrl | scancode::Linux::KeyRightCtrl => XMods::ControlMask,
@@ -348,7 +348,7 @@ impl XMods {
// unchanged // unchanged
if pressed_mask.is_empty() && locked_mask.is_empty() { if pressed_mask.is_empty() && locked_mask.is_empty() {
log::trace!("{:#?} is not a modifier key", key); log::trace!("{key:#?} is not a modifier key");
return false; return false;
} }
match state { match state {

View File

@@ -6,12 +6,12 @@ use x11::{
}; };
use input_event::{ use input_event::{
Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, Event, KeyboardEvent, PointerEvent,
}; };
use crate::error::EmulationError; use crate::error::EmulationError;
use super::{error::X11EmulationCreationError, Emulation, EmulationHandle}; use super::{Emulation, EmulationHandle, error::X11EmulationCreationError};
pub(crate) struct X11Emulation { pub(crate) struct X11Emulation {
display: *mut xlib::Display, display: *mut xlib::Display,
@@ -23,7 +23,7 @@ impl X11Emulation {
pub(crate) fn new() -> Result<Self, X11EmulationCreationError> { pub(crate) fn new() -> Result<Self, X11EmulationCreationError> {
let display = unsafe { let display = unsafe {
match xlib::XOpenDisplay(ptr::null()) { match xlib::XOpenDisplay(ptr::null()) {
d if d == ptr::null::<xlib::Display>() as *mut xlib::Display => { d if std::ptr::eq(d, ptr::null_mut::<xlib::Display>()) => {
Err(X11EmulationCreationError::OpenDisplay) Err(X11EmulationCreationError::OpenDisplay)
} }
display => Ok(display), display => Ok(display),

View File

@@ -1,7 +1,7 @@
use ashpd::{ use ashpd::{
desktop::{ desktop::{
remote_desktop::{Axis, DeviceType, KeyState, RemoteDesktop},
PersistMode, Session, PersistMode, Session,
remote_desktop::{Axis, DeviceType, KeyState, RemoteDesktop},
}, },
zbus::AsyncDrop, zbus::AsyncDrop,
}; };
@@ -15,7 +15,7 @@ use input_event::{
use crate::error::EmulationError; use crate::error::EmulationError;
use super::{error::XdpEmulationCreationError, Emulation, EmulationHandle}; use super::{Emulation, EmulationHandle, error::XdpEmulationCreationError};
pub(crate) struct DesktopPortalEmulation<'a> { pub(crate) struct DesktopPortalEmulation<'a> {
proxy: RemoteDesktop<'a>, proxy: RemoteDesktop<'a>,
@@ -143,7 +143,6 @@ impl Emulation for DesktopPortalEmulation<'_> {
impl AsyncDrop for DesktopPortalEmulation<'_> { impl AsyncDrop for DesktopPortalEmulation<'_> {
#[doc = r" Perform the async cleanup."] #[doc = r" Perform the async cleanup."]
#[must_use]
#[allow(clippy::type_complexity, clippy::type_repetition_in_bounds)] #[allow(clippy::type_complexity, clippy::type_repetition_in_bounds)]
fn async_drop<'async_trait>( fn async_drop<'async_trait>(
self, self,

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ use super::ClientData;
#[properties(wrapper_type = super::ClientObject)] #[properties(wrapper_type = super::ClientObject)]
pub struct ClientObject { pub struct ClientObject {
#[property(name = "handle", get, set, type = ClientHandle, member = handle)] #[property(name = "handle", get, set, type = ClientHandle, member = handle)]
#[property(name = "hostname", get, set, type = String, member = hostname)] #[property(name = "hostname", get, set, type = Option<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::DEFAULT_PORT; use lan_mouse_ipc::{DEFAULT_PORT, Position};
use super::ClientObject; use super::ClientObject;
@@ -15,25 +15,32 @@ glib::wrapper! {
} }
impl ClientRow { impl ClientRow {
pub fn new(_client_object: &ClientObject) -> Self { pub fn new(client_object: &ClientObject) -> Self {
Object::builder().build() let client_row: Self = 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>| {
@@ -43,72 +50,48 @@ 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>| { .transform_to(|_, v: Option<String>| v.or(Some("<span font_style=\"italic\" font_weight=\"light\" foreground=\"darkgrey\">no hostname!</span>".to_string())))
if let Some(hostname) = v {
Some(hostname)
} else {
Some("<span font_style=\"italic\" font_weight=\"light\" foreground=\"darkgrey\">no hostname!</span>".to_string())
}
})
.sync_create() .sync_create()
.build(); .build();
// bind port to port edit field
let port_binding = client_object let port_binding = client_object
.bind_property("port", &self.imp().port.get(), "text") .bind_property("port", &self.imp().port.get(), "text")
.transform_from(|_, v: String| {
if v.is_empty() {
Some(DEFAULT_PORT as u32)
} else {
Some(v.parse::<u16>().unwrap_or(DEFAULT_PORT) as u32)
}
})
.transform_to(|_, v: u32| { .transform_to(|_, v: u32| {
if v == 4242 { if v == DEFAULT_PORT as u32 {
Some("".to_string()) Some("".to_string())
} else { } else {
Some(v.to_string()) Some(v.to_string())
} }
}) })
.bidirectional()
.sync_create() .sync_create()
.build(); .build();
// bind port to subtitle
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(1), "right" => Some(1u32),
"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",
@@ -118,6 +101,7 @@ 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>| {
@@ -146,4 +130,24 @@ 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

@@ -1,13 +1,16 @@
use std::cell::RefCell; use std::cell::RefCell;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use adw::{prelude::*, ActionRow, ComboRow}; use adw::{ActionRow, ComboRow, prelude::*};
use glib::{subclass::InitializingObject, Binding}; use glib::{Binding, subclass::InitializingObject};
use gtk::glib::clone;
use gtk::glib::subclass::Signal; use gtk::glib::subclass::Signal;
use gtk::{glib, Button, CompositeTemplate, Switch}; use gtk::glib::{SignalHandlerId, clone};
use gtk::{Button, CompositeTemplate, Entry, Switch, glib};
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 {
@@ -28,6 +31,11 @@ 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]
@@ -59,17 +67,61 @@ 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-dns").build(), Signal::builder("request-activate")
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(),
] ]
}) })
} }
@@ -78,22 +130,97 @@ impl ObjectImpl for ClientRow {
#[gtk::template_callbacks] #[gtk::template_callbacks]
impl ClientRow { impl ClientRow {
#[template_callback] #[template_callback]
fn handle_client_set_state(&self, state: bool, _switch: &Switch) -> bool { fn handle_activate_switch(&self, state: bool, _switch: &Switch) -> bool {
log::debug!("state change -> requesting update"); self.obj().emit_by_name::<()>("request-activate", &[&state]);
self.obj().emit_by_name::<()>("request-update", &[&state]);
true // dont run default handler true // dont run default handler
} }
#[template_callback] #[template_callback]
fn handle_request_dns(&self, _: Button) { fn handle_request_dns(&self, _: &Button) {
self.obj().emit_by_name::<()>("request-dns", &[]); self.obj().emit_by_name::<()>("request-dns", &[]);
} }
#[template_callback] #[template_callback]
fn handle_client_delete(&self, _button: &Button) { fn handle_client_delete(&self, _button: &Button) {
log::debug!("delete button pressed -> requesting delete");
self.obj().emit_by_name::<()>("request-delete", &[]); self.obj().emit_by_name::<()>("request-delete", &[]);
} }
fn handle_port_changed(&self, port_entry: &Entry) {
if let Ok(port) = port_entry.text().parse::<u16>() {
self.obj()
.emit_by_name::<()>("request-port-change", &[&(port as u32)]);
}
}
fn handle_hostname_changed(&self, hostname_entry: &Entry) {
self.obj()
.emit_by_name::<()>("request-hostname-change", &[&hostname_entry.text()]);
}
fn handle_position_changed(&self, position: &ComboRow) {
self.obj()
.emit_by_name("request-position-change", &[&position.selected()])
}
pub(super) fn set_hostname(&self, hostname: Option<String>) {
let position = self.hostname.position();
let handler = self.hostname_change_handler.borrow();
let handler = handler.as_ref().expect("signal handler");
self.hostname.block_signal(handler);
self.client_object
.borrow_mut()
.as_mut()
.expect("client object")
.set_property("hostname", hostname);
self.hostname.unblock_signal(handler);
self.hostname.set_position(position);
}
pub(super) fn set_port(&self, port: u16) {
let position = self.port.position();
let handler = self.port_change_handler.borrow();
let handler = handler.as_ref().expect("signal handler");
self.port.block_signal(handler);
self.client_object
.borrow_mut()
.as_mut()
.expect("client object")
.set_port(port as u32);
self.port.unblock_signal(handler);
self.port.set_position(position);
}
pub(super) fn set_pos(&self, pos: Position) {
let handler = self.position_change_handler.borrow();
let handler = handler.as_ref().expect("signal handler");
self.position.block_signal(handler);
self.client_object
.borrow_mut()
.as_mut()
.expect("client object")
.set_position(pos.to_string());
self.position.unblock_signal(handler);
}
pub(super) fn set_active(&self, active: bool) {
let handler = self.set_state_handler.borrow();
let handler = handler.as_ref().expect("signal handler");
self.enable_switch.block_signal(handler);
self.client_object
.borrow_mut()
.as_mut()
.expect("client object")
.set_active(active);
self.enable_switch.unblock_signal(handler);
}
pub(super) fn set_dns_state(&self, resolved: bool) {
if resolved {
self.dns_button.set_css_classes(&["success"])
} else {
self.dns_button.set_css_classes(&["warning"])
}
}
} }
impl WidgetImpl for ClientRow {} impl WidgetImpl for ClientRow {}

View File

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

View File

@@ -4,8 +4,9 @@ use adw::prelude::*;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use glib::subclass::InitializingObject; use glib::subclass::InitializingObject;
use gtk::{ use gtk::{
Button, CompositeTemplate, Text,
glib::{self, subclass::Signal}, glib::{self, subclass::Signal},
template_callbacks, Button, CompositeTemplate, Text, template_callbacks,
}; };
#[derive(CompositeTemplate, Default)] #[derive(CompositeTemplate, Default)]
@@ -51,9 +52,11 @@ impl ObjectImpl for FingerprintWindow {
fn signals() -> &'static [Signal] { fn signals() -> &'static [Signal] {
static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new(); static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
SIGNALS.get_or_init(|| { SIGNALS.get_or_init(|| {
vec![Signal::builder("confirm-clicked") vec![
.param_types([String::static_type(), String::static_type()]) Signal::builder("confirm-clicked")
.build()] .param_types([String::static_type(), String::static_type()])
.build(),
]
}) })
} }
} }

View File

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

View File

@@ -1,11 +1,11 @@
use std::cell::RefCell; use std::cell::RefCell;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use adw::{prelude::*, ActionRow}; use adw::{ActionRow, prelude::*};
use glib::{subclass::InitializingObject, Binding}; use glib::{Binding, subclass::InitializingObject};
use gtk::glib::clone; use gtk::glib::clone;
use gtk::glib::subclass::Signal; use gtk::glib::subclass::Signal;
use gtk::{glib, Button, CompositeTemplate}; use gtk::{Button, CompositeTemplate, glib};
use std::sync::OnceLock; use std::sync::OnceLock;
#[derive(CompositeTemplate, Default)] #[derive(CompositeTemplate, Default)]

View File

@@ -1,3 +1,4 @@
mod authorization_window;
mod client_object; mod client_object;
mod client_row; mod client_row;
mod fingerprint_window; mod fingerprint_window;
@@ -9,18 +10,24 @@ use std::{env, process, str};
use window::Window; use window::Window;
use lan_mouse_ipc::{FrontendEvent, FrontendRequest}; use lan_mouse_ipc::FrontendEvent;
use adw::Application; use adw::Application;
use gtk::{ use gtk::{IconTheme, gdk::Display, glib::clone, prelude::*};
gdk::Display, glib::clone, prelude::*, subclass::prelude::ObjectSubclassIsExt, IconTheme,
};
use gtk::{gio, glib, prelude::ApplicationExt}; use gtk::{gio, glib, prelude::ApplicationExt};
use self::client_object::ClientObject; use self::client_object::ClientObject;
use self::key_object::KeyObject; use self::key_object::KeyObject;
pub fn run() -> glib::ExitCode { use thiserror::Error;
#[derive(Error, Debug)]
pub enum GtkError {
#[error("gtk frontend exited with non zero exit code: {0}")]
NonZeroExitCode(i32),
}
pub fn run() -> Result<(), GtkError> {
log::debug!("running gtk frontend"); log::debug!("running gtk frontend");
#[cfg(windows)] #[cfg(windows)]
let ret = std::thread::Builder::new() let ret = std::thread::Builder::new()
@@ -33,13 +40,10 @@ pub fn run() -> glib::ExitCode {
#[cfg(not(windows))] #[cfg(not(windows))]
let ret = gtk_main(); let ret = gtk_main();
if ret == glib::ExitCode::FAILURE { match ret {
log::error!("frontend exited with failure"); glib::ExitCode::SUCCESS => Ok(()),
} else { e => Err(GtkError::NonZeroExitCode(e.value())),
log::info!("frontend exited successfully");
} }
ret
} }
fn gtk_main() -> glib::ExitCode { fn gtk_main() -> glib::ExitCode {
@@ -49,7 +53,11 @@ fn gtk_main() -> glib::ExitCode {
.application_id("de.feschber.LanMouse") .application_id("de.feschber.LanMouse")
.build(); .build();
app.connect_startup(|_| load_icons()); app.connect_startup(|app| {
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![];
@@ -62,6 +70,33 @@ 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() {
@@ -96,54 +131,41 @@ 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::Changed(handle) => {
window.request(FrontendRequest::GetState(handle));
}
FrontendEvent::Created(handle, client, state) => { FrontendEvent::Created(handle, client, state) => {
window.new_client(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) => { FrontendEvent::Error(e) => window.show_toast(e.as_str()),
window.show_toast(e.as_str()); FrontendEvent::Enumerate(clients) => window.update_client_list(clients),
FrontendEvent::PortChanged(port, msg) => window.update_port(port, msg),
FrontendEvent::CaptureStatus(s) => window.set_capture(s.into()),
FrontendEvent::EmulationStatus(s) => window.set_emulation(s.into()),
FrontendEvent::AuthorizedUpdated(keys) => window.set_authorized_keys(keys),
FrontendEvent::PublicKeyFingerprint(fp) => window.set_pk_fp(&fp),
FrontendEvent::ConnectionAttempt { fingerprint } => {
window.request_authorization(&fingerprint);
} }
FrontendEvent::Enumerate(clients) => { FrontendEvent::DeviceConnected {
for (handle, client, state) in clients { fingerprint: _,
if window.client_idx(handle).is_some() { addr,
window.update_client_config(handle, client); } => {
window.update_client_state(handle, state); window.show_toast(format!("device connected: {addr}").as_str());
} else {
window.new_client(handle, client, state);
}
}
} }
FrontendEvent::PortChanged(port, msg) => { FrontendEvent::DeviceEntered {
match msg { fingerprint: _,
None => window.show_toast(format!("port changed: {port}").as_str()), addr,
Some(msg) => window.show_toast(msg.as_str()), pos,
} } => {
window.imp().set_port(port); window.show_toast(format!("device entered: {addr} ({pos})").as_str());
} }
FrontendEvent::CaptureStatus(s) => { FrontendEvent::IncomingDisconnected(addr) => {
window.set_capture(s.into()); window.show_toast(format!("{addr} disconnected").as_str());
} }
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

@@ -4,19 +4,21 @@ use std::collections::HashMap;
use adw::prelude::*; use adw::prelude::*;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use glib::{clone, Object}; use glib::{Object, clone};
use gtk::{ use gtk::{
gio, NoSelection, gio,
glib::{self, closure_local}, glib::{self, closure_local},
ListBox, NoSelection,
}; };
use lan_mouse_ipc::{ use lan_mouse_ipc::{
ClientConfig, ClientHandle, ClientState, FrontendRequest, FrontendRequestWriter, Position, ClientConfig, ClientHandle, ClientState, DEFAULT_PORT, FrontendRequest, FrontendRequestWriter,
DEFAULT_PORT, Position,
}; };
use crate::{fingerprint_window::FingerprintWindow, key_object::KeyObject, key_row::KeyRow}; use crate::{
authorization_window::AuthorizationWindow, fingerprint_window::FingerprintWindow,
key_object::KeyObject, key_row::KeyRow,
};
use super::{client_object::ClientObject, client_row::ClientRow}; use super::{client_object::ClientObject, client_row::ClientRow};
@@ -28,7 +30,7 @@ glib::wrapper! {
} }
impl Window { impl Window {
pub(crate) fn new(app: &adw::Application, conn: FrontendRequestWriter) -> Self { pub(super) 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 +40,7 @@ impl Window {
window window
} }
pub fn clients(&self) -> gio::ListStore { fn clients(&self) -> gio::ListStore {
self.imp() self.imp()
.clients .clients
.borrow() .borrow()
@@ -46,7 +48,7 @@ impl Window {
.expect("Could not get clients") .expect("Could not get clients")
} }
pub fn authorized(&self) -> gio::ListStore { fn authorized(&self) -> gio::ListStore {
self.imp() self.imp()
.authorized .authorized
.borrow() .borrow()
@@ -62,6 +64,14 @@ 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));
@@ -112,16 +122,57 @@ 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-update", "request-hostname-change",
false,
closure_local!(
#[strong]
window,
move |row: ClientRow, hostname: String| {
log::debug!("request-hostname-change");
if let Some(client) = window.client_by_idx(row.index() as u32) {
let hostname = Some(hostname).filter(|s| !s.is_empty());
/* changed in response to FrontendEvent
* -> do not request additional update */
window.request(FrontendRequest::UpdateHostname(
client.handle(),
hostname,
));
}
}
),
);
row.connect_closure(
"request-port-change",
false,
closure_local!(
#[strong]
window,
move |row: ClientRow, port: u32| {
if let Some(client) = window.client_by_idx(row.index() as u32) {
window.request(FrontendRequest::UpdatePort(
client.handle(),
port as u16,
));
}
}
),
);
row.connect_closure(
"request-activate",
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) {
window.request_client_activate(&client, active); log::debug!(
window.request_client_update(&client); "request: {} client",
window.request_client_state(&client); if active { "activating" } else { "deactivating" }
);
window.request(FrontendRequest::Activate(
client.handle(),
active,
));
} }
} }
), ),
@@ -134,7 +185,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_client_delete(&client); window.request(FrontendRequest::Delete(client.handle()));
} }
} }
), ),
@@ -147,9 +198,31 @@ 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_client_update(&client); window.request(FrontendRequest::ResolveDns(
window.request_dns(&client); client.get_data().handle,
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,
));
} }
} }
), ),
@@ -160,10 +233,14 @@ 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
pub fn update_placeholder_visibility(&self) { 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 {
@@ -172,7 +249,7 @@ impl Window {
}); });
} }
pub fn update_auth_placeholder_visibility(&self) { 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 {
@@ -181,10 +258,6 @@ 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);
@@ -197,24 +270,46 @@ impl Window {
row row
} }
pub fn new_client(&self, handle: ClientHandle, client: ClientConfig, state: ClientState) { pub(super) fn new_client(
&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 fn client_idx(&self, handle: ClientHandle) -> Option<usize> { pub(super) fn update_client_list(
self.clients().iter::<ClientObject>().position(|c| { &self,
if let Ok(c) = c { clients: Vec<(ClientHandle, ClientConfig, ClientState)>,
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 {
false self.new_client(handle, client, state);
} }
}) }
} }
pub fn delete_client(&self, handle: ClientHandle) { pub(super) fn update_port(&self, port: u16, msg: Option<String>) {
if let Some(msg) = msg {
self.show_toast(msg.as_str());
}
self.imp().set_port(port);
}
fn client_idx(&self, handle: ClientHandle) -> Option<usize> {
self.clients()
.iter::<ClientObject>()
.position(|c| c.ok().map(|c| c.handle() == handle).unwrap_or_default())
}
pub(super) fn delete_client(&self, handle: ClientHandle) {
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;
@@ -226,46 +321,31 @@ impl Window {
} }
} }
pub fn update_client_config(&self, handle: ClientHandle, client: ClientConfig) { pub(super) fn update_client_config(&self, handle: ClientHandle, client: ClientConfig) {
let Some(idx) = self.client_idx(handle) else { let Some(row) = self.row_for_handle(handle) else {
log::warn!("could not find client with handle {}", handle); log::warn!("could not find row for handle {handle}");
return; return;
}; };
let client_object = self.clients().item(idx as u32).unwrap(); row.set_hostname(client.hostname);
let client_object: &ClientObject = client_object.downcast_ref().unwrap(); row.set_port(client.port);
let data = client_object.get_data(); row.set_position(client.pos);
/* 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 fn update_client_state(&self, handle: ClientHandle, state: ClientState) { pub(super) fn update_client_state(&self, handle: ClientHandle, state: ClientState) {
let Some(idx) = self.client_idx(handle) else { let Some(row) = self.row_for_handle(handle) else {
log::warn!("could not find client with handle {}", handle); log::warn!("could not find row for handle {handle}");
return;
};
let Some(client_object) = self.client_object_for_handle(handle) else {
log::warn!("could not find row for handle {handle}");
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();
if state.active != data.active { /* activation state */
client_object.set_active(state.active); row.set_active(state.active);
log::debug!("set active to {}", state.active);
}
if state.resolving != data.resolving { /* dns state */
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
@@ -276,22 +356,23 @@ impl Window {
client_object.set_ips(ips); client_object.set_ips(ips);
} }
pub fn update_dns_state(&self, handle: ClientHandle, resolved: bool) { fn client_object_for_handle(&self, handle: ClientHandle) -> Option<ClientObject> {
let Some(idx) = self.client_idx(handle) else { self.client_idx(handle)
log::warn!("could not find client with handle {}", handle); .and_then(|i| self.client_by_idx(i as u32))
return; }
};
let list_box: ListBox = self.imp().client_list.get(); fn row_for_handle(&self, handle: ClientHandle) -> Option<ClientRow> {
let row = list_box.row_at_index(idx as i32).unwrap(); self.client_idx(handle)
let client_row: ClientRow = row.downcast().expect("expected ClientRow Object"); .and_then(|i| self.row_by_idx(i as i32))
if resolved { }
client_row.imp().dns_button.set_css_classes(&["success"])
} else { fn update_dns_state(&self, handle: ClientHandle, resolved: bool) {
client_row.imp().dns_button.set_css_classes(&["warning"]) if let Some(client_row) = self.row_for_handle(handle) {
client_row.set_dns_state(resolved);
} }
} }
pub fn request_port_change(&self) { fn request_port_change(&self) {
let port = self let port = self
.imp() .imp()
.port_entry .port_entry
@@ -303,56 +384,20 @@ impl Window {
self.request(FrontendRequest::ChangePort(port)); self.request(FrontendRequest::ChangePort(port));
} }
pub fn request_capture(&self) { fn request_capture(&self) {
self.request(FrontendRequest::EnableCapture); self.request(FrontendRequest::EnableCapture);
} }
pub fn request_emulation(&self) { fn request_emulation(&self) {
self.request(FrontendRequest::EnableEmulation); self.request(FrontendRequest::EnableEmulation);
} }
pub fn request_client_state(&self, client: &ClientObject) { fn request_client_create(&self) {
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);
} }
pub fn request_dns(&self, client: &ClientObject) { fn open_fingerprint_dialog(&self, fp: Option<String>) {
self.request(FrontendRequest::ResolveDns(client.get_data().handle)); let window = FingerprintWindow::new(fp);
}
pub fn request_client_update(&self, client: &ClientObject) {
let handle = client.handle();
let data = client.get_data();
let position = Position::try_from(data.position.as_str()).expect("invalid position");
let hostname = data.hostname;
let port = data.port as u16;
for event in [
FrontendRequest::UpdateHostname(handle, hostname),
FrontendRequest::UpdatePosition(handle, position),
FrontendRequest::UpdatePort(handle, port),
] {
self.request(event);
}
}
pub fn request_client_activate(&self, client: &ClientObject, active: bool) {
self.request(FrontendRequest::Activate(client.handle(), active));
}
pub fn request_client_delete(&self, client: &ClientObject) {
self.request(FrontendRequest::Delete(client.handle()));
}
pub fn open_fingerprint_dialog(&self) {
let window = FingerprintWindow::new();
window.set_transient_for(Some(self)); window.set_transient_for(Some(self));
window.connect_closure( window.connect_closure(
"confirm-clicked", "confirm-clicked",
@@ -369,15 +414,15 @@ impl Window {
window.present(); window.present();
} }
pub fn request_fingerprint_add(&self, desc: String, fp: String) { fn request_fingerprint_add(&self, desc: String, fp: String) {
self.request(FrontendRequest::AuthorizeKey(desc, fp)); self.request(FrontendRequest::AuthorizeKey(desc, fp));
} }
pub fn request_fingerprint_remove(&self, fp: String) { fn request_fingerprint_remove(&self, fp: String) {
self.request(FrontendRequest::RemoveAuthorizedKey(fp)); self.request(FrontendRequest::RemoveAuthorizedKey(fp));
} }
pub fn request(&self, request: FrontendRequest) { 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) {
@@ -385,18 +430,18 @@ impl Window {
}; };
} }
pub fn show_toast(&self, msg: &str) { pub(super) 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 fn set_capture(&self, active: bool) { pub(super) 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 fn set_emulation(&self, active: bool) { pub(super) 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();
} }
@@ -411,7 +456,7 @@ impl Window {
.set_visible(!capture || !emulation); .set_visible(!capture || !emulation);
} }
pub(crate) fn set_authorized_keys(&self, fingerprints: HashMap<String, String>) { pub(super) 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();
@@ -423,7 +468,36 @@ impl Window {
self.update_auth_placeholder_visibility(); self.update_auth_placeholder_visibility();
} }
pub(crate) fn set_pk_fp(&self, fingerprint: &str) { pub(super) fn set_pk_fp(&self, fingerprint: &str) {
self.imp().fingerprint_row.set_subtitle(fingerprint); self.imp().fingerprint_row.set_subtitle(fingerprint);
} }
pub(super) fn request_authorization(&self, fingerprint: &str) {
if let Some(w) = self.imp().authorization_window.borrow_mut().take() {
w.close();
}
let window = AuthorizationWindow::new(fingerprint);
window.set_transient_for(Some(self));
window.connect_closure(
"confirm-clicked",
false,
closure_local!(
#[strong(rename_to = parent)]
self,
move |w: AuthorizationWindow, fp: String| {
w.close();
parent.open_fingerprint_dialog(Some(fp));
}
),
);
window.connect_closure(
"cancel-clicked",
false,
closure_local!(move |w: AuthorizationWindow| {
w.close();
}),
);
window.present();
self.imp().authorization_window.replace(Some(window));
}
} }

View File

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

View File

@@ -1,7 +1,7 @@
use crate::{ConnectionError, FrontendEvent, FrontendRequest, IpcError}; use crate::{ConnectionError, FrontendEvent, FrontendRequest, IpcError};
use std::{ use std::{
cmp::min, cmp::min,
io::{self, prelude::*, BufReader, LineWriter, Lines}, io::{self, BufReader, LineWriter, Lines, prelude::*},
thread, thread,
time::Duration, time::Duration,
}; };

View File

@@ -1,8 +1,7 @@
use crate::{ConnectionError, FrontendEvent, FrontendRequest, IpcError}; use crate::{ConnectionError, FrontendEvent, FrontendRequest, IpcError};
use std::{ use std::{
cmp::min, cmp::min,
io, task::{Poll, ready},
task::{ready, Poll},
time::Duration, time::Duration,
}; };
@@ -47,7 +46,7 @@ impl Stream for AsyncFrontendEventReader {
} }
impl AsyncFrontendRequestWriter { impl AsyncFrontendRequestWriter {
pub async fn request(&mut self, request: FrontendRequest) -> Result<(), io::Error> { pub async fn request(&mut self, request: FrontendRequest) -> Result<(), IpcError> {
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');
@@ -57,8 +56,16 @@ 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 = wait_for_service().await?; let stream = if let Some(duration) = timeout {
tokio::select! {
s = wait_for_service() => s?,
_ = tokio::time::sleep(duration) => return Err(ConnectionError::Timeout),
}
} else {
wait_for_service().await?
};
#[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

@@ -20,8 +20,8 @@ mod connect;
mod connect_async; mod connect_async;
mod listen; mod listen;
pub use connect::{connect, FrontendEventReader, FrontendRequestWriter}; pub use connect::{FrontendEventReader, FrontendRequestWriter, connect};
pub use connect_async::{connect_async, AsyncFrontendEventReader, AsyncFrontendRequestWriter}; pub use connect_async::{AsyncFrontendEventReader, AsyncFrontendRequestWriter, connect_async};
pub use listen::AsyncFrontendListener; pub use listen::AsyncFrontendListener;
#[derive(Debug, Error)] #[derive(Debug, Error)]
@@ -30,6 +30,8 @@ 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)]
@@ -57,6 +59,7 @@ pub enum IpcError {
pub const DEFAULT_PORT: u16 = 4242; pub const DEFAULT_PORT: u16 = 4242;
#[derive(Debug, Default, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)] #[derive(Debug, Default, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Position { pub enum Position {
#[default] #[default]
Left, Left,
@@ -177,8 +180,6 @@ 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
@@ -201,10 +202,21 @@ pub enum FrontendEvent {
AuthorizedUpdated(HashMap<String, String>), AuthorizedUpdated(HashMap<String, String>),
/// public key fingerprint of this device /// public key fingerprint of this device
PublicKeyFingerprint(String), PublicKeyFingerprint(String),
/// incoming connected /// new device connected
IncomingConnected(String, SocketAddr, Position), DeviceConnected {
addr: SocketAddr,
fingerprint: String,
},
/// incoming device entered the screen
DeviceEntered {
fingerprint: String,
addr: SocketAddr,
pos: Position,
},
/// incoming disconnected /// incoming disconnected
IncomingDisconnected(SocketAddr), IncomingDisconnected(SocketAddr),
/// failed connection attempt (approval for fingerprint required)
ConnectionAttempt { fingerprint: String },
} }
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
@@ -229,8 +241,6 @@ 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
@@ -241,6 +251,8 @@ 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

@@ -1,4 +1,4 @@
use futures::{stream::SelectAll, Stream, StreamExt}; use futures::{Stream, StreamExt, stream::SelectAll};
#[cfg(unix)] #[cfg(unix)]
use std::path::PathBuf; use std::path::PathBuf;
use std::{ use std::{
@@ -45,7 +45,7 @@ impl AsyncFrontendListener {
let (socket_path, listener) = { let (socket_path, listener) = {
let socket_path = crate::default_socket_path()?; let socket_path = crate::default_socket_path()?;
log::debug!("remove socket: {:?}", socket_path); log::debug!("remove socket: {socket_path:?}");
if socket_path.exists() { if socket_path.exists() {
// try to connect to see if some other instance // try to connect to see if some other instance
// of lan-mouse is already running // of lan-mouse is already running
@@ -63,7 +63,7 @@ impl AsyncFrontendListener {
Ok(ls) => ls, Ok(ls) => ls,
// some other lan-mouse instance has bound the socket in the meantime // some other lan-mouse instance has bound the socket in the meantime
Err(e) if e.kind() == ErrorKind::AddrInUse => { Err(e) if e.kind() == ErrorKind::AddrInUse => {
return Err(IpcListenerCreationError::AlreadyRunning) return Err(IpcListenerCreationError::AlreadyRunning);
} }
Err(e) => return Err(IpcListenerCreationError::Bind(e)), Err(e) => return Err(IpcListenerCreationError::Bind(e)),
}; };
@@ -75,7 +75,7 @@ impl AsyncFrontendListener {
Ok(ls) => ls, Ok(ls) => ls,
// some other lan-mouse instance has bound the socket in the meantime // some other lan-mouse instance has bound the socket in the meantime
Err(e) if e.kind() == ErrorKind::AddrInUse => { Err(e) if e.kind() == ErrorKind::AddrInUse => {
return Err(IpcListenerCreationError::AlreadyRunning) return Err(IpcListenerCreationError::AlreadyRunning);
} }
Err(e) => return Err(IpcListenerCreationError::Bind(e)), Err(e) => return Err(IpcListenerCreationError::Bind(e)),
}; };

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

93
scripts/copy-macos-dylib.sh Executable file
View File

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

42
scripts/makeicns.sh Executable file
View File

@@ -0,0 +1,42 @@
#!/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

@@ -10,8 +10,8 @@ use input_capture::{
}; };
use input_event::scancode; use input_event::scancode;
use lan_mouse_proto::ProtoEvent; use lan_mouse_proto::ProtoEvent;
use local_channel::mpsc::{channel, Receiver, Sender}; use local_channel::mpsc::{Receiver, Sender, channel};
use tokio::task::{spawn_local, JoinHandle}; use tokio::task::{JoinHandle, spawn_local};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use crate::connect::LanMouseConnection; use crate::connect::LanMouseConnection;

View File

@@ -1,12 +1,16 @@
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};
pub async fn run(config: Config) -> Result<(), InputCaptureError> { #[derive(Args, Clone, Debug, Eq, PartialEq)]
pub struct TestCaptureArgs {}
pub async fn run(config: Config, _args: TestCaptureArgs) -> Result<(), InputCaptureError> {
log::info!("running input capture test"); log::info!("running input capture test");
log::info!("creating input capture"); log::info!("creating input capture");
let backend = config.capture_backend.map(|b| b.into()); let backend = config.capture_backend().map(|b| b.into());
loop { loop {
let mut input_capture = InputCapture::new(backend).await?; let mut input_capture = InputCapture::new(backend).await?;
log::info!("creating clients"); log::info!("creating clients");

View File

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

View File

@@ -1,7 +1,7 @@
use crate::client::ClientManager; use crate::client::ClientManager;
use lan_mouse_ipc::{ClientHandle, DEFAULT_PORT}; use lan_mouse_ipc::{ClientHandle, DEFAULT_PORT};
use lan_mouse_proto::{ProtoEvent, MAX_EVENT_SIZE}; use lan_mouse_proto::{MAX_EVENT_SIZE, ProtoEvent};
use local_channel::mpsc::{channel, Receiver, Sender}; use local_channel::mpsc::{Receiver, Sender, channel};
use std::{ use std::{
cell::RefCell, cell::RefCell,
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
@@ -15,7 +15,7 @@ use thiserror::Error;
use tokio::{ use tokio::{
net::UdpSocket, net::UdpSocket,
sync::Mutex, sync::Mutex,
task::{spawn_local, JoinSet}, task::{JoinSet, spawn_local},
}; };
use webrtc_dtls::{ use webrtc_dtls::{
config::{Config, ExtendedMasterSecretType}, config::{Config, ExtendedMasterSecretType},

View File

@@ -1,9 +1,9 @@
use std::net::IpAddr; use std::{collections::HashMap, net::IpAddr};
use local_channel::mpsc::{channel, Receiver, Sender}; use local_channel::mpsc::{Receiver, Sender, channel};
use tokio::task::{spawn_local, JoinHandle}; use tokio::task::{JoinHandle, spawn_local};
use hickory_resolver::{error::ResolveError, TokioAsyncResolver}; use hickory_resolver::{ResolveError, TokioResolver};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use lan_mouse_ipc::ClientHandle; use lan_mouse_ipc::ClientHandle;
@@ -26,19 +26,21 @@ pub(crate) enum DnsEvent {
} }
struct DnsTask { struct DnsTask {
resolver: TokioAsyncResolver, resolver: TokioResolver,
request_rx: Receiver<DnsRequest>, request_rx: Receiver<DnsRequest>,
event_tx: Sender<DnsEvent>, event_tx: Sender<DnsEvent>,
cancellation_token: CancellationToken, cancellation_token: CancellationToken,
active_tasks: HashMap<ClientHandle, JoinHandle<()>>,
} }
impl DnsResolver { impl DnsResolver {
pub(crate) fn new() -> Result<Self, ResolveError> { pub(crate) fn new() -> Result<Self, ResolveError> {
let resolver = TokioAsyncResolver::tokio_from_system_conf()?; let resolver = TokioResolver::builder_tokio()?.build();
let (request_tx, request_rx) = channel(); let (request_tx, request_rx) = channel();
let (event_tx, event_rx) = channel(); let (event_tx, event_rx) = channel();
let cancellation_token = CancellationToken::new(); let cancellation_token = CancellationToken::new();
let dns_task = DnsTask { let dns_task = DnsTask {
active_tasks: Default::default(),
resolver, resolver,
request_rx, request_rx,
event_tx, event_tx,
@@ -81,6 +83,14 @@ 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");
@@ -90,7 +100,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();
tokio::task::spawn_local(async move { let task = 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<_>>());
@@ -101,6 +111,7 @@ impl DnsTask {
_ = cancellation_token.cancelled() => {}, _ = cancellation_token.cancelled() => {},
} }
}); });
self.active_tasks.insert(handle, task);
} }
} }
} }

View File

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

View File

@@ -1,4 +1,5 @@
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;
@@ -7,10 +8,20 @@ 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;
pub async fn run(config: Config) -> Result<(), InputEmulationError> { #[derive(Args, Clone, Debug, Eq, PartialEq)]
pub struct TestEmulationArgs {
#[arg(long)]
mouse: bool,
#[arg(long)]
keyboard: bool,
#[arg(long)]
scroll: bool,
}
pub async fn run(config: Config, _args: TestEmulationArgs) -> Result<(), InputEmulationError> {
log::info!("running input emulation test"); log::info!("running input emulation test");
let backend = config.emulation_backend.map(|b| b.into()); let backend = config.emulation_backend().map(|b| b.into());
let mut emulation = InputEmulation::new(backend).await?; let mut emulation = InputEmulation::new(backend).await?;
emulation.create(0).await; emulation.create(0).await;

View File

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

View File

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

View File

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