Compare commits

..

31 Commits

Author SHA1 Message Date
Ferdinand Schober
253a6d1911 improve reliability of connections 2025-11-03 16:53:47 +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
68 changed files with 2728 additions and 2220 deletions

View File

@@ -94,7 +94,9 @@ jobs:
- name: Install cargo bundle
run: cargo install cargo-bundle
- name: Bundle
run: cargo bundle --release
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
@@ -120,7 +122,9 @@ jobs:
- name: Install cargo bundle
run: cargo install cargo-bundle
- name: Bundle
run: cargo bundle --release
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

View File

@@ -112,7 +112,9 @@ jobs:
- name: Install cargo bundle
run: cargo install cargo-bundle
- name: Bundle
run: cargo bundle
run: |
cargo bundle
scripts/copy-macos-dylib.sh
- name: Zip bundle
run: |
cd target/debug/bundle/osx
@@ -142,7 +144,9 @@ jobs:
- name: Install cargo bundle
run: cargo install cargo-bundle
- name: Bundle
run: cargo bundle
run: |
cargo bundle
scripts/copy-macos-dylib.sh
- name: Zip bundle
run: |
cd target/debug/bundle/osx

View File

@@ -90,7 +90,9 @@ jobs:
- name: Install cargo bundle
run: cargo install cargo-bundle
- name: Bundle
run: cargo bundle --release
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
@@ -116,7 +118,9 @@ jobs:
- name: Install cargo bundle
run: cargo install cargo-bundle
- name: Bundle
run: cargo bundle --release
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

4
.gitignore vendored
View File

@@ -8,3 +8,7 @@ result
*.pem
*.csr
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

2056
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -81,15 +81,37 @@ paru -S lan-mouse-git
- flake: [README.md](./nix/README.md)
</details>
<details>
<summary>Fedora</summary>
You can install Lan Mouse from the [Terra Repository](https://terra.fyralabs.com).
After enabling Terra:
```sh
dnf install lan-mouse
```
</details>
<details>
<summary>MacOS</summary>
- Download the package for your Mac (Intel or ARM) from the releases page
- Unzip it
- Remove the quarantine with `xattr -rd com.apple.quarantine "Lan Mouse.app"`
- Launch the app
- Grant accessibility permissions in System Preferences
</details>
<details>
<summary>Manual Installation</summary>
First make sure to [install the necessary dependencies](#installing-dependencies).
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).
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).
@@ -161,7 +183,15 @@ For a detailed list of available features, checkout the [Cargo.toml](./Cargo.tom
<summary>MacOS</summary>
```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>
@@ -268,19 +298,17 @@ If the device still can not be entered, make sure you have UDP port `4242` (or t
<details>
<summary>Command Line Interface</summary>
The cli interface can be enabled using `--frontend cli` as commandline arguments.
Type `help` to list the available commands.
E.g.:
The cli interface can be accessed by passing `cli` as a commandline argument.
Use
```sh
$ cargo run --release -- --frontend cli
(...)
> connect <host> left|right|top|bottom
(...)
> list
(...)
> activate 0
lan-mouse cli help
```
to list the available commands and
```sh
lan-mouse cli <cmd> help
```
for information on how to use a specific command.
</details>
<details>
@@ -288,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).
To do so, add `--daemon` to the commandline args:
To do so, use the `daemon` subcommand:
```sh
lan-mouse --daemon
lan-mouse daemon
```
In order to start lan-mouse with a graphical session automatically,
@@ -326,9 +354,6 @@ release_bind = [ "KeyA", "KeyS", "KeyD", "KeyF" ]
# optional port (defaults to 4242)
port = 4242
# # optional frontend -> defaults to gtk if available
# # possible values are "cli" and "gtk"
# frontend = "gtk"
# list of authorized tls certificate fingerprints that
# are accepted for incoming traffic
@@ -336,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"
# define a client on the right side with host name "iridium"
[right]
[[clients]]
# position (left | right | top | bottom)
position = "right"
# hostname
hostname = "iridium"
# activate this client immediately when lan-mouse is started
@@ -345,7 +372,8 @@ activate_on_startup = true
ips = ["192.168.178.156"]
# 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,
# at least one ip address needs to be specified.
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,14 +1,10 @@
# example configuration
# capture_backend = "LayerShell"
# release bind
release_bind = ["KeyA", "KeyS", "KeyD", "KeyF"]
# configure release bind
release_bind = [ "KeyA", "KeyS", "KeyD", "KeyF" ]
# optional port (defaults to 4242)
port = 4242
# optional frontend -> defaults to gtk if available
# frontend = "gtk"
# list of authorized tls certificate fingerprints that
# are accepted for incoming traffic
@@ -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"
# define a client on the right side with host name "iridium"
[right]
[[clients]]
# position (left | right | top | bottom)
position = "right"
# hostname
hostname = "iridium"
# activate this client immediately when lan-mouse is started
activate_on_startup = true
# optional list of (known) ip addresses
ips = ["192.168.178.156"]
# define a client on the left side with IP address 192.168.178.189
[left]
[[clients]]
position = "left"
# The hostname is optional: When no hostname is specified,
# at least one ip address needs to be specified.
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": {
"nixpkgs": {
"locked": {
"lastModified": 1740560979,
"narHash": "sha256-Vr3Qi346M+8CjedtbyUevIGDZW8LcA1fTG0ugPY/Hic=",
"lastModified": 1752687322,
"narHash": "sha256-RKwfXA4OZROjBTQAl9WOZQFm7L8Bo93FQwSJpAiSRvo=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "5135c59491985879812717f4c9fea69604e7f26f",
"rev": "6e987485eb2c77e5dcc5af4e3c70843711ef9251",
"type": "github"
},
"original": {
@@ -29,11 +29,11 @@
]
},
"locked": {
"lastModified": 1740623427,
"narHash": "sha256-3SdPQrZoa4odlScFDUHd4CUPQ/R1gtH4Mq9u8CBiK8M=",
"lastModified": 1752806774,
"narHash": "sha256-4cHeoR2roN7d/3J6gT+l6o7J2hTrBIUiCwVdDNMeXzE=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "d342e8b5fd88421ff982f383c853f0fc78a847ab",
"rev": "3c90219b3ba1c9790c45a078eae121de48a39c55",
"type": "github"
},
"original": {

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ use std::{
io::{self, ErrorKind},
os::fd::{AsFd, RawFd},
pin::Pin,
task::{ready, Context, Poll},
task::{Context, Poll, ready},
};
use tokio::io::unix::AsyncFd;
@@ -45,9 +45,10 @@ use wayland_protocols_wlr::layer_shell::v1::client::{
};
use wayland_client::{
Connection, Dispatch, DispatchError, EventQueue, QueueHandle, WEnum,
backend::{ReadEventsGuard, WaylandError},
delegate_noop,
globals::{registry_queue_init, Global, GlobalList, GlobalListContents},
globals::{Global, GlobalList, GlobalListContents, registry_queue_init},
protocol::{
wl_buffer, wl_compositor,
wl_keyboard::{self, WlKeyboard},
@@ -58,7 +59,6 @@ use wayland_client::{
wl_seat, wl_shm, wl_shm_pool,
wl_surface::WlSurface,
},
Connection, Dispatch, DispatchError, EventQueue, QueueHandle, WEnum,
};
use input_event::{Event, KeyboardEvent, PointerEvent};
@@ -66,8 +66,8 @@ use input_event::{Event, KeyboardEvent, PointerEvent};
use crate::{CaptureError, CaptureEvent};
use super::{
error::{LayerShellCaptureCreationError, WaylandBindError},
Capture, Position,
error::{LayerShellCaptureCreationError, WaylandBindError},
};
struct Globals {
@@ -535,7 +535,7 @@ impl State {
fn update_windows(&mut self) {
log::info!("active outputs: ");
for output in self.outputs.iter().filter(|o| o.info.is_some()) {
log::info!(" * {}", output);
log::info!(" * {output}");
}
self.active_windows.clear();
@@ -582,17 +582,17 @@ impl Inner {
match self.queue.dispatch_pending(&mut self.state) {
Ok(_) => {}
Err(DispatchError::Backend(WaylandError::Io(e))) => {
log::error!("Wayland Error: {}", e);
log::error!("Wayland Error: {e}");
}
Err(DispatchError::Backend(e)) => {
panic!("backend error: {}", e);
panic!("backend error: {e}");
}
Err(DispatchError::BadMessage {
sender_id,
interface,
opcode,
}) => {
panic!("bad message {}, {} , {}", sender_id, interface, opcode);
panic!("bad message {sender_id}, {interface} , {opcode}");
}
}
}
@@ -813,7 +813,7 @@ impl Dispatch<WlPointer, ()> for State {
})),
));
}
wl_pointer::Event::Frame {} => {
wl_pointer::Event::Frame => {
// TODO properly handle frame events
// we simply insert a frame event on the client side
// after each event for now
@@ -974,7 +974,7 @@ impl Dispatch<ZxdgOutputV1, u32> for State {
.find(|o| o.global.name == *name)
.expect("output");
log::debug!("xdg_output {name} - {:?}", event);
log::debug!("xdg_output {name} - {event:?}");
match event {
zxdg_output_v1::Event::LogicalPosition { x, y } => {
output.pending_info.position = (x, y);
@@ -1010,7 +1010,7 @@ impl Dispatch<WlOutput, u32> for State {
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
log::debug!("wl_output {name} - {:?}", event);
log::debug!("wl_output {name} - {event:?}");
if let wl_output::Event::Done = event {
state.update_output_info(*name);
}

View File

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

View File

@@ -1,10 +1,10 @@
use ashpd::{
desktop::{
Session,
input_capture::{
Activated, ActivatedBarrier, Barrier, BarrierID, Capabilities, InputCapture, Region,
Zones,
},
Session,
},
enumflags2::BitFlags,
};
@@ -28,8 +28,8 @@ use std::{
};
use tokio::{
sync::{
mpsc::{self, Receiver, Sender},
Notify,
mpsc::{self, Receiver, Sender},
},
task::JoinHandle,
};
@@ -42,8 +42,8 @@ use input_event::Event;
use crate::CaptureEvent;
use super::{
error::{CaptureError, LibeiCaptureCreationError},
Capture as LanMouseInputCapture, Position,
error::{CaptureError, LibeiCaptureCreationError},
};
/* there is a bug in xdg-remote-desktop-portal-gnome / mutter that
@@ -587,9 +587,13 @@ impl LanMouseInputCapture for LibeiInputCapture<'_> {
self.cancellation_token.cancel();
let task = &mut self.capture_task;
log::debug!("waiting for capture to terminate...");
let res = task.await.expect("libei task panic");
log::debug!("done!");
let res = if !task.is_finished() {
task.await.expect("libei task panic")
} else {
Ok(())
};
self.terminated = true;
log::debug!("done!");
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 bitflags::bitflags;
use core_foundation::base::{kCFAllocatorDefault, CFRelease};
use core_foundation::date::CFTimeInterval;
use core_foundation::number::{kCFBooleanTrue, CFBooleanRef};
use core_foundation::runloop::{kCFRunLoopCommonModes, CFRunLoop, CFRunLoopSource};
use core_foundation::string::{kCFStringEncodingUTF8, CFStringCreateWithCString, CFStringRef};
use core_graphics::base::{kCGErrorSuccess, CGError};
use core_graphics::display::{CGDisplay, CGPoint};
use core_graphics::event::{
CGEvent, CGEventFlags, CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement,
CGEventTapProxy, CGEventType, EventField,
use core_foundation::{
base::{CFRelease, kCFAllocatorDefault},
date::CFTimeInterval,
number::{CFBooleanRef, kCFBooleanTrue},
runloop::{CFRunLoop, CFRunLoopSource, kCFRunLoopCommonModes},
string::{CFStringCreateWithCString, CFStringRef, kCFStringEncodingUTF8},
};
use core_graphics::{
base::{CGError, kCGErrorSuccess},
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 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 libc::c_void;
use once_cell::unsync::Lazy;
use std::collections::HashSet;
use std::ffi::{c_char, CString};
use std::pin::Pin;
use std::sync::Arc;
use std::task::{ready, Context, Poll};
use std::thread::{self};
use tokio::sync::mpsc::{self, Receiver, Sender};
use tokio::sync::{oneshot, Mutex};
use std::{
collections::HashSet,
ffi::{CString, c_char},
pin::Pin,
sync::Arc,
task::{Context, Poll, ready},
thread::{self},
};
use tokio::sync::{
Mutex,
mpsc::{self, Receiver, Sender},
oneshot,
};
#[derive(Debug, Default)]
struct Bounds {
@@ -37,9 +46,16 @@ struct Bounds {
#[derive(Debug)]
struct InputCaptureState {
/// active capture positions
active_clients: Lazy<HashSet<Position>>,
/// the currently entered capture position, if any
current_pos: Option<Position>,
/// position where the cursor was captured
enter_position: Option<CGPoint>,
/// bounds of the input capture area
bounds: Bounds,
/// current state of modifier keys
modifier_state: XMods,
}
#[derive(Debug)]
@@ -56,7 +72,9 @@ impl InputCaptureState {
let mut res = Self {
active_clients: Lazy::new(HashSet::new),
current_pos: None,
enter_position: None,
bounds: Bounds::default(),
modifier_state: Default::default(),
};
res.update_bounds()?;
Ok(res)
@@ -96,45 +114,34 @@ impl InputCaptureState {
Ok(())
}
// We can't disable mouse movement when in a client so we need to reset the cursor position
// to the edge of the screen, the cursor will be hidden but we dont want it to appear in a
// random location when we exit the client
fn reset_mouse_position(&self, event: &CGEvent) -> Result<(), CaptureError> {
if let Some(pos) = self.current_pos {
let location = event.location();
let edge_offset = 1.0;
/// start the input capture by
fn start_capture(&mut self, event: &CGEvent, position: Position) -> Result<(), CaptureError> {
let mut location = event.location();
let edge_offset = 1.0;
// move cursor location to display bounds
match position {
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
// will carry the delta from the warp so only half the delta is needed to move the cursor
let delta_y = event.get_double_value_field(EventField::MOUSE_EVENT_DELTA_Y) / 2.0;
let delta_x = event.get_double_value_field(EventField::MOUSE_EVENT_DELTA_X) / 2.0;
/// resets the cursor to the position, where the capture started
fn reset_cursor(&mut self) -> Result<(), CaptureError> {
let pos = self.enter_position.expect("capture active");
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;
let mut new_y = location.y + delta_y;
fn hide_cursor(&self) -> Result<(), CaptureError> {
CGDisplay::hide_cursor(&CGDisplay::main()).map_err(CaptureError::CoreGraphics)
}
match pos {
Position::Left => {
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)
fn show_cursor(&self) -> Result<(), CaptureError> {
CGDisplay::show_cursor(&CGDisplay::main()).map_err(CaptureError::CoreGraphics)
}
async fn handle_producer_event(
@@ -145,15 +152,13 @@ impl InputCaptureState {
match producer_event {
ProducerEvent::Release => {
if self.current_pos.is_some() {
CGDisplay::show_cursor(&CGDisplay::main())
.map_err(CaptureError::CoreGraphics)?;
self.show_cursor()?;
self.current_pos = None;
}
}
ProducerEvent::Grab(pos) => {
if self.current_pos.is_none() {
CGDisplay::hide_cursor(&CGDisplay::main())
.map_err(CaptureError::CoreGraphics)?;
self.hide_cursor()?;
self.current_pos = Some(pos);
}
}
@@ -163,8 +168,7 @@ impl InputCaptureState {
ProducerEvent::Destroy(p) => {
if let Some(current) = self.current_pos {
if current == p {
CGDisplay::show_cursor(&CGDisplay::main())
.map_err(CaptureError::CoreGraphics)?;
self.show_cursor()?;
self.current_pos = None;
};
}
@@ -180,6 +184,7 @@ fn get_events(
ev_type: &CGEventType,
ev: &CGEvent,
result: &mut Vec<CaptureEvent>,
modifier_state: &mut XMods,
) -> Result<(), CaptureError> {
fn map_pointer_event(ev: &CGEvent) -> PointerEvent {
PointerEvent::Motion {
@@ -215,29 +220,42 @@ fn get_events(
})));
}
CGEventType::FlagsChanged => {
let mut mods = XMods::empty();
let mut depressed = XMods::empty();
let mut mods_locked = XMods::empty();
let cg_flags = ev.get_flags();
if cg_flags.contains(CGEventFlags::CGEventFlagShift) {
mods |= XMods::ShiftMask;
depressed |= XMods::ShiftMask;
}
if cg_flags.contains(CGEventFlags::CGEventFlagControl) {
mods |= XMods::ControlMask;
depressed |= XMods::ControlMask;
}
if cg_flags.contains(CGEventFlags::CGEventFlagAlternate) {
mods |= XMods::Mod1Mask;
depressed |= XMods::Mod1Mask;
}
if cg_flags.contains(CGEventFlags::CGEventFlagCommand) {
mods |= XMods::Mod4Mask;
depressed |= XMods::Mod4Mask;
}
if cg_flags.contains(CGEventFlags::CGEventFlagAlphaShift) {
mods |= XMods::LockMask;
depressed |= 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 {
depressed: mods.bits(),
depressed: depressed.bits(),
latched: 0,
locked: mods_locked.bits(),
group: 0,
@@ -348,7 +366,7 @@ fn create_event_tap<'a>(
move |_proxy: CGEventTapProxy, event_type: CGEventType, cg_ev: &CGEvent| {
log::trace!("Got event from tap: {event_type:?}");
let mut state = client_state.blocking_lock();
let mut pos = None;
let mut capture_position = None;
let mut res_events = vec![];
if matches!(
@@ -365,22 +383,34 @@ fn create_event_tap<'a>(
// Are we in a client?
if let Some(current_pos) = state.current_pos {
pos = Some(current_pos);
get_events(&event_type, cg_ev, &mut res_events).unwrap_or_else(|e| {
capture_position = Some(current_pos);
get_events(
&event_type,
cg_ev,
&mut res_events,
&mut state.modifier_state,
)
.unwrap_or_else(|e| {
log::error!("Failed to get events: {e}");
});
// Keep (hidden) cursor at the edge of the screen
if matches!(event_type, CGEventType::MouseMoved) {
state.reset_mouse_position(cg_ev).unwrap_or_else(|e| {
log::error!("Failed to reset mouse position: {e}");
})
if matches!(
event_type,
CGEventType::MouseMoved
| CGEventType::LeftMouseDragged
| CGEventType::RightMouseDragged
| CGEventType::OtherMouseDragged
) {
state.reset_cursor().unwrap_or_else(|e| log::warn!("{e}"));
}
}
// Did we cross a barrier?
else if matches!(event_type, CGEventType::MouseMoved) {
} else if matches!(event_type, CGEventType::MouseMoved) {
// Did we cross a barrier?
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);
notify_tx
.blocking_send(ProducerEvent::Grab(new_pos))
@@ -388,17 +418,17 @@ fn create_event_tap<'a>(
}
}
if let Some(pos) = pos {
if let Some(pos) = capture_position {
res_events.iter().for_each(|e| {
event_tx
.blocking_send((pos, *e))
.expect("Failed to send event");
// error must be ignored, since the event channel
// may already be closed when the InputCapture instance is dropped.
let _ = event_tx.blocking_send((pos, *e));
});
// 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
cg_ev.set_type(CGEventType::Null);
}
Some(cg_ev.to_owned())
CallbackResult::Replace(cg_ev.to_owned())
};
let tap = CGEventTap::new(
@@ -411,7 +441,7 @@ fn create_event_tap<'a>(
.map_err(|_| MacosCaptureCreationError::EventTapCreation)?;
let tap_source: CFRunLoopSource = tap
.mach_port
.mach_port()
.create_runloop_source(0)
.expect("Failed creating loop source");
@@ -426,8 +456,8 @@ fn event_tap_thread(
client_state: Arc<Mutex<InputCaptureState>>,
event_tx: Sender<(Position, CaptureEvent)>,
notify_tx: Sender<ProducerEvent>,
ready: std::sync::mpsc::Sender<Result<(), MacosCaptureCreationError>>,
exit: oneshot::Sender<Result<(), &'static str>>,
ready: std::sync::mpsc::Sender<Result<CFRunLoop, MacosCaptureCreationError>>,
exit: oneshot::Sender<()>,
) {
let _tap = match create_event_tap(client_state, notify_tx, event_tx) {
Err(e) => {
@@ -435,18 +465,22 @@ fn event_tap_thread(
return;
}
Ok(tap) => {
ready.send(Ok(())).expect("channel closed");
let run_loop = CFRunLoop::get_current();
ready.send(Ok(run_loop)).expect("channel closed");
tap
}
};
log::debug!("running CFRunLoop...");
CFRunLoop::run_current();
log::debug!("event tap thread exiting!...");
let _ = exit.send(Err("tap thread exited"));
let _ = exit.send(());
}
pub struct MacOSInputCapture {
event_rx: Receiver<(Position, CaptureEvent)>,
notify_tx: Sender<ProducerEvent>,
run_loop: CFRunLoop,
}
impl MacOSInputCapture {
@@ -475,36 +509,41 @@ impl MacOSInputCapture {
});
// 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 {
loop {
tokio::select! {
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;
state.handle_producer_event(producer_event).await.unwrap_or_else(|e| {
log::error!("Failed to handle producer event: {e}");
})
}
res = &mut tap_exit_rx => {
if let Err(e) = res.expect("channel closed") {
log::error!("Tap thread failed: {:?}", e);
break;
}
}
_ = &mut tap_exit_rx => break,
}
}
// show cursor
let _ = CGDisplay::show_cursor(&CGDisplay::main());
});
Ok(Self {
event_rx,
notify_tx,
run_loop,
})
}
}
impl Drop for MacOSInputCapture {
fn drop(&mut self) {
self.run_loop.stop();
}
}
#[async_trait]
impl Capture for MacOSInputCapture {
async fn create(&mut self, pos: Position) -> Result<(), CaptureError> {

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,25 +1,25 @@
use futures::{future, StreamExt};
use futures::{StreamExt, future};
use std::{
io,
os::{fd::OwnedFd, unix::net::UnixStream},
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex, RwLock,
atomic::{AtomicBool, Ordering},
},
time::{SystemTime, UNIX_EPOCH},
};
use tokio::task::JoinHandle;
use ashpd::desktop::{
remote_desktop::{DeviceType, RemoteDesktop},
PersistMode, Session,
remote_desktop::{DeviceType, RemoteDesktop},
};
use async_trait::async_trait;
use reis::{
ei::{
self, button::ButtonState, handshake::ContextType, keyboard::KeyState, Button, Keyboard,
Pointer, Scroll,
self, Button, Keyboard, Pointer, Scroll, button::ButtonState, handshake::ContextType,
keyboard::KeyState,
},
event::{self, Connection, DeviceCapability, DeviceEvent, EiEvent, SeatEvent},
tokio::EiConvertEventStream,
@@ -29,7 +29,7 @@ use input_event::{Event, KeyboardEvent, PointerEvent};
use crate::error::EmulationError;
use super::{error::LibeiEmulationCreationError, Emulation, EmulationHandle};
use super::{Emulation, EmulationHandle, error::LibeiEmulationCreationError};
#[derive(Clone, Default)]
struct Devices {
@@ -50,8 +50,8 @@ pub(crate) struct LibeiEmulation<'a> {
session: Session<'a, RemoteDesktop<'a>>,
}
async fn get_ei_fd<'a>(
) -> Result<(RemoteDesktop<'a>, Session<'a, RemoteDesktop<'a>>, OwnedFd), ashpd::Error> {
async fn get_ei_fd<'a>()
-> Result<(RemoteDesktop<'a>, Session<'a, RemoteDesktop<'a>>, OwnedFd), ashpd::Error> {
let remote_desktop = RemoteDesktop::new().await?;
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 bitflags::bitflags;
use core_graphics::base::CGFloat;
@@ -10,25 +10,37 @@ use core_graphics::event::{
ScrollEventUnit,
};
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 std::cell::Cell;
use std::ops::{Index, IndexMut};
use std::rc::Rc;
use std::sync::Arc;
use std::time::Duration;
use std::time::{Duration, Instant};
use tokio::{sync::Notify, task::JoinHandle};
use super::error::MacOSEmulationCreationError;
const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500);
const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32);
const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(500);
pub(crate) struct MacOSEmulation {
/// global event source for all events
event_source: CGEventSource,
/// task handle for key repeats
repeat_task: Option<JoinHandle<()>>,
/// current state of the mouse buttons
button_state: ButtonState,
/// button previously pressed
previous_button: Option<CGMouseButton>,
/// timestamp of previous click (button down)
previous_button_click: Option<Instant>,
/// click state, i.e. number of clicks in quick succession
button_click_state: i64,
/// current modifier state
modifier_state: Rc<Cell<XMods>>,
/// notify to cancel key repeats
notify_repeat_task: Arc<Notify>,
}
@@ -74,6 +86,9 @@ impl MacOSEmulation {
Ok(Self {
event_source,
button_state,
previous_button: None,
previous_button_click: None,
button_click_state: 0,
repeat_task: None,
notify_repeat_task: Arc::new(Notify::new()),
modifier_state: Rc::new(Cell::new(XMods::empty())),
@@ -89,6 +104,9 @@ impl MacOSEmulation {
// there can only be one repeating key and it's
// always the last to be pressed
self.cancel_repeat_task().await;
// initial key event
key_event(self.event_source.clone(), key, 1, self.modifier_state.get());
// repeat task
let event_source = self.event_source.clone();
let notify = self.notify_repeat_task.clone();
let modifiers = self.modifier_state.clone();
@@ -161,12 +179,12 @@ fn get_display_at_point(x: CGFloat, y: CGFloat) -> Option<CGDirectDisplayID> {
};
if error != 0 {
log::warn!("error getting displays at point ({}, {}): {}", x, y, error);
log::warn!("error getting displays at point ({x}, {y}): {error}");
return Option::None;
}
if display_count == 0 {
log::debug!("no displays found at point ({}, {})", x, y);
log::debug!("no displays found at point ({x}, {y})");
return Option::None;
}
@@ -224,150 +242,167 @@ impl Emulation for MacOSEmulation {
event: Event,
_handle: EmulationHandle,
) -> Result<(), EmulationError> {
log::trace!("{event:?}");
match event {
Event::Pointer(pointer_event) => match pointer_event {
PointerEvent::Motion { time: _, dx, dy } => {
let mut mouse_location = match self.get_mouse_location() {
Some(l) => l,
None => {
log::warn!("could not get mouse location!");
return Ok(());
}
};
Event::Pointer(pointer_event) => {
match pointer_event {
PointerEvent::Motion { time: _, dx, dy } => {
let mut mouse_location = match self.get_mouse_location() {
Some(l) => l,
None => {
log::warn!("could not get mouse location!");
return Ok(());
}
};
let (new_mouse_x, new_mouse_y) =
clamp_to_screen_space(mouse_location.x, mouse_location.y, dx, dy);
let (new_mouse_x, new_mouse_y) =
clamp_to_screen_space(mouse_location.x, mouse_location.y, dx, dy);
mouse_location.x = new_mouse_x;
mouse_location.y = new_mouse_y;
mouse_location.x = new_mouse_x;
mouse_location.y = new_mouse_y;
let mut event_type = CGEventType::MouseMoved;
if self.button_state.left {
event_type = CGEventType::LeftMouseDragged
} else if self.button_state.right {
event_type = CGEventType::RightMouseDragged
} else if self.button_state.center {
event_type = CGEventType::OtherMouseDragged
};
let event = match CGEvent::new_mouse_event(
self.event_source.clone(),
event_type,
mouse_location,
CGMouseButton::Left,
) {
Ok(e) => e,
Err(_) => {
log::warn!("mouse event creation failed!");
return Ok(());
}
};
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.post(CGEventTapLocation::HID);
}
PointerEvent::Button {
time: _,
button,
state,
} => {
let (event_type, mouse_button) = match (button, state) {
(b, 1) if b == input_event::BTN_LEFT => {
(CGEventType::LeftMouseDown, CGMouseButton::Left)
}
(b, 0) if b == input_event::BTN_LEFT => {
(CGEventType::LeftMouseUp, CGMouseButton::Left)
}
(b, 1) if b == input_event::BTN_RIGHT => {
(CGEventType::RightMouseDown, CGMouseButton::Right)
}
(b, 0) if b == input_event::BTN_RIGHT => {
(CGEventType::RightMouseUp, CGMouseButton::Right)
}
(b, 1) if b == input_event::BTN_MIDDLE => {
(CGEventType::OtherMouseDown, CGMouseButton::Center)
}
(b, 0) if b == input_event::BTN_MIDDLE => {
(CGEventType::OtherMouseUp, CGMouseButton::Center)
}
_ => {
log::warn!("invalid button event: {button},{state}");
return Ok(());
}
};
// store button state
self.button_state[mouse_button] = state == 1;
let mut event_type = CGEventType::MouseMoved;
if self.button_state.left {
event_type = CGEventType::LeftMouseDragged
} else if self.button_state.right {
event_type = CGEventType::RightMouseDragged
} else if self.button_state.center {
event_type = CGEventType::OtherMouseDragged
};
let event = match CGEvent::new_mouse_event(
self.event_source.clone(),
event_type,
mouse_location,
CGMouseButton::Left,
) {
Ok(e) => e,
Err(_) => {
log::warn!("mouse event creation failed!");
return Ok(());
}
};
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.post(CGEventTapLocation::HID);
}
PointerEvent::Button {
time: _,
button,
state,
} => {
let (event_type, mouse_button) = match (button, state) {
(BTN_LEFT, 1) => (CGEventType::LeftMouseDown, CGMouseButton::Left),
(BTN_LEFT, 0) => (CGEventType::LeftMouseUp, CGMouseButton::Left),
(BTN_RIGHT, 1) => (CGEventType::RightMouseDown, CGMouseButton::Right),
(BTN_RIGHT, 0) => (CGEventType::RightMouseUp, CGMouseButton::Right),
(BTN_MIDDLE, 1) => (CGEventType::OtherMouseDown, CGMouseButton::Center),
(BTN_MIDDLE, 0) => (CGEventType::OtherMouseUp, CGMouseButton::Center),
_ => {
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();
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(());
// update previous button state
if state == 1 {
if self.previous_button.is_some_and(|b| b.eq(&mouse_button))
&& self
.previous_button_click
.is_some_and(|i| i.elapsed() < DOUBLE_CLICK_INTERVAL)
{
self.button_click_state += 1;
} else {
self.button_click_state = 1;
}
self.previous_button = Some(mouse_button);
self.previous_button_click = Some(Instant::now());
}
};
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: _,
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);
// reset button click state in case it's not a button event
if !matches!(pointer_event, PointerEvent::Button { .. }) {
self.button_click_state = 0;
}
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 {
KeyboardEvent::Key {
time: _,
@@ -381,18 +416,12 @@ impl Emulation for MacOSEmulation {
return Ok(());
}
};
update_modifiers(&self.modifier_state, key, state);
match state {
// pressed
1 => self.spawn_repeat_task(code).await,
_ => self.cancel_repeat_task().await,
}
update_modifiers(&self.modifier_state, key, state);
key_event(
self.event_source.clone(),
code,
state,
self.modifier_state.get(),
);
}
KeyboardEvent::Modifiers {
depressed,
@@ -416,6 +445,21 @@ impl Emulation for MacOSEmulation {
async fn terminate(&mut self) {}
}
trait ButtonEq {
fn eq(&self, other: &Self) -> bool;
}
impl ButtonEq for CGMouseButton {
fn eq(&self, other: &Self) -> bool {
matches!(
(self, other),
(CGMouseButton::Left, CGMouseButton::Left)
| (CGMouseButton::Right, CGMouseButton::Right)
| (CGMouseButton::Center, CGMouseButton::Center)
)
}
}
fn update_modifiers(modifiers: &Cell<XMods>, key: u32, state: u8) -> bool {
if let Ok(key) = scancode::Linux::try_from(key) {
let mask = match key {

View File

@@ -1,22 +1,22 @@
use super::error::{EmulationError, WindowsEmulationCreationError};
use input_event::{
scancode, 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,
scancode,
};
use async_trait::async_trait;
use std::ops::BitOrAssign;
use std::time::Duration;
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::{
INPUT, INPUT_KEYBOARD, INPUT_MOUSE, KEYBDINPUT, KEYEVENTF_KEYUP, KEYEVENTF_SCANCODE,
MOUSEEVENTF_HWHEEL, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MIDDLEDOWN,
MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP,
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 super::{Emulation, EmulationHandle};

View File

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

View File

@@ -6,12 +6,12 @@ use x11::{
};
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 super::{error::X11EmulationCreationError, Emulation, EmulationHandle};
use super::{Emulation, EmulationHandle, error::X11EmulationCreationError};
pub(crate) struct X11Emulation {
display: *mut xlib::Display,
@@ -23,7 +23,7 @@ impl X11Emulation {
pub(crate) fn new() -> Result<Self, X11EmulationCreationError> {
let display = unsafe {
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)
}
display => Ok(display),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

@@ -4,7 +4,7 @@ use adw::prelude::*;
use adw::subclass::prelude::*;
use gtk::glib::{self, Object};
use lan_mouse_ipc::{Position, DEFAULT_PORT};
use lan_mouse_ipc::{DEFAULT_PORT, Position};
use super::ClientObject;

View File

@@ -1,11 +1,11 @@
use std::cell::RefCell;
use adw::subclass::prelude::*;
use adw::{prelude::*, ActionRow, ComboRow};
use glib::{subclass::InitializingObject, Binding};
use adw::{ActionRow, ComboRow, prelude::*};
use glib::{Binding, subclass::InitializingObject};
use gtk::glib::subclass::Signal;
use gtk::glib::{clone, SignalHandlerId};
use gtk::{glib, Button, CompositeTemplate, Entry, Switch};
use gtk::glib::{SignalHandlerId, clone};
use gtk::{Button, CompositeTemplate, Entry, Switch, glib};
use lan_mouse_ipc::Position;
use std::sync::OnceLock;

View File

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

View File

@@ -4,8 +4,9 @@ use adw::prelude::*;
use adw::subclass::prelude::*;
use glib::subclass::InitializingObject;
use gtk::{
Button, CompositeTemplate, Text,
glib::{self, subclass::Signal},
template_callbacks, Button, CompositeTemplate, Text,
template_callbacks,
};
#[derive(CompositeTemplate, Default)]
@@ -51,9 +52,11 @@ impl ObjectImpl for FingerprintWindow {
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(), String::static_type()])
.build()]
vec![
Signal::builder("confirm-clicked")
.param_types([String::static_type(), String::static_type()])
.build(),
]
})
}
}

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
mod authorization_window;
mod client_object;
mod client_row;
mod fingerprint_window;
@@ -12,13 +13,21 @@ use window::Window;
use lan_mouse_ipc::FrontendEvent;
use adw::Application;
use gtk::{gdk::Display, glib::clone, prelude::*, IconTheme};
use gtk::{IconTheme, gdk::Display, glib::clone, prelude::*};
use gtk::{gio, glib, prelude::ApplicationExt};
use self::client_object::ClientObject;
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");
#[cfg(windows)]
let ret = std::thread::Builder::new()
@@ -31,13 +40,10 @@ pub fn run() -> glib::ExitCode {
#[cfg(not(windows))]
let ret = gtk_main();
if ret == glib::ExitCode::FAILURE {
log::error!("frontend exited with failure");
} else {
log::info!("frontend exited successfully");
match ret {
glib::ExitCode::SUCCESS => Ok(()),
e => Err(GtkError::NonZeroExitCode(e.value())),
}
ret
}
fn gtk_main() -> glib::ExitCode {
@@ -141,8 +147,21 @@ fn build_ui(app: &Application) {
FrontendEvent::EmulationStatus(s) => window.set_emulation(s.into()),
FrontendEvent::AuthorizedUpdated(keys) => window.set_authorized_keys(keys),
FrontendEvent::PublicKeyFingerprint(fp) => window.set_pk_fp(&fp),
FrontendEvent::IncomingConnected(_fingerprint, addr, pos) => {
window.show_toast(format!("device connected: {addr} ({pos})").as_str());
FrontendEvent::ConnectionAttempt { fingerprint } => {
window.request_authorization(&fingerprint);
}
FrontendEvent::DeviceConnected {
fingerprint: _,
addr,
} => {
window.show_toast(format!("device connected: {addr}").as_str());
}
FrontendEvent::DeviceEntered {
fingerprint: _,
addr,
pos,
} => {
window.show_toast(format!("device entered: {addr} ({pos})").as_str());
}
FrontendEvent::IncomingDisconnected(addr) => {
window.show_toast(format!("{addr} disconnected").as_str());

View File

@@ -4,19 +4,21 @@ use std::collections::HashMap;
use adw::prelude::*;
use adw::subclass::prelude::*;
use glib::{clone, Object};
use glib::{Object, clone};
use gtk::{
gio,
NoSelection, gio,
glib::{self, closure_local},
NoSelection,
};
use lan_mouse_ipc::{
ClientConfig, ClientHandle, ClientState, FrontendRequest, FrontendRequestWriter, Position,
DEFAULT_PORT,
ClientConfig, ClientHandle, ClientState, DEFAULT_PORT, FrontendRequest, FrontendRequestWriter,
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};
@@ -126,7 +128,7 @@ impl Window {
#[strong]
window,
move |row: ClientRow, hostname: String| {
log::info!("request-hostname-change");
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
@@ -163,7 +165,7 @@ impl Window {
window,
move |row: ClientRow, active: bool| {
if let Some(client) = window.client_by_idx(row.index() as u32) {
log::info!(
log::debug!(
"request: {} client",
if active { "activating" } else { "deactivating" }
);
@@ -321,7 +323,7 @@ impl Window {
pub(super) fn update_client_config(&self, handle: ClientHandle, client: ClientConfig) {
let Some(row) = self.row_for_handle(handle) else {
log::warn!("could not find row for handle {}", handle);
log::warn!("could not find row for handle {handle}");
return;
};
row.set_hostname(client.hostname);
@@ -331,11 +333,11 @@ impl Window {
pub(super) fn update_client_state(&self, handle: ClientHandle, state: ClientState) {
let Some(row) = self.row_for_handle(handle) else {
log::warn!("could not find row for handle {}", handle);
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);
log::warn!("could not find row for handle {handle}");
return;
};
@@ -394,8 +396,8 @@ impl Window {
self.request(FrontendRequest::Create);
}
fn open_fingerprint_dialog(&self) {
let window = FingerprintWindow::new();
fn open_fingerprint_dialog(&self, fp: Option<String>) {
let window = FingerprintWindow::new(fp);
window.set_transient_for(Some(self));
window.connect_closure(
"confirm-clicked",
@@ -469,4 +471,33 @@ impl Window {
pub(super) fn set_pk_fp(&self, fingerprint: &str) {
self.imp().fingerprint_row.set_subtitle(fingerprint);
}
pub(super) fn request_authorization(&self, fingerprint: &str) {
if let Some(w) = self.imp().authorization_window.borrow_mut().take() {
w.close();
}
let window = AuthorizationWindow::new(fingerprint);
window.set_transient_for(Some(self));
window.connect_closure(
"confirm-clicked",
false,
closure_local!(
#[strong(rename_to = parent)]
self,
move |w: AuthorizationWindow, fp: String| {
w.close();
parent.open_fingerprint_dialog(Some(fp));
}
),
);
window.connect_closure(
"cancel-clicked",
false,
closure_local!(move |w: AuthorizationWindow| {
w.close();
}),
);
window.present();
self.imp().authorization_window.replace(Some(window));
}
}

View File

@@ -1,12 +1,14 @@
use std::cell::{Cell, RefCell};
use adw::subclass::prelude::*;
use adw::{prelude::*, ActionRow, PreferencesGroup, ToastOverlay};
use adw::{ActionRow, PreferencesGroup, ToastOverlay, prelude::*};
use glib::subclass::InitializingObject;
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)]
#[template(resource = "/de/feschber/LanMouse/window.ui")]
@@ -49,6 +51,7 @@ pub struct Window {
pub port: Cell<u16>,
pub capture_active: Cell<bool>,
pub emulation_active: Cell<bool>,
pub authorization_window: RefCell<Option<AuthorizationWindow>>,
}
#[glib::object_subclass]
@@ -149,7 +152,7 @@ impl Window {
#[template_callback]
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) {

View File

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

View File

@@ -1,8 +1,7 @@
use crate::{ConnectionError, FrontendEvent, FrontendRequest, IpcError};
use std::{
cmp::min,
io,
task::{ready, Poll},
task::{Poll, ready},
time::Duration,
};
@@ -47,7 +46,7 @@ impl Stream for AsyncFrontendEventReader {
}
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();
log::debug!("requesting: {json}");
json.push('\n');
@@ -57,8 +56,16 @@ impl AsyncFrontendRequestWriter {
}
pub async fn connect_async(
timeout: Option<Duration>,
) -> 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)]
let (rx, tx): (ReadHalf<UnixStream>, WriteHalf<UnixStream>) = tokio::io::split(stream);
#[cfg(windows)]

View File

@@ -20,8 +20,8 @@ mod connect;
mod connect_async;
mod listen;
pub use connect::{connect, FrontendEventReader, FrontendRequestWriter};
pub use connect_async::{connect_async, AsyncFrontendEventReader, AsyncFrontendRequestWriter};
pub use connect::{FrontendEventReader, FrontendRequestWriter, connect};
pub use connect_async::{AsyncFrontendEventReader, AsyncFrontendRequestWriter, connect_async};
pub use listen::AsyncFrontendListener;
#[derive(Debug, Error)]
@@ -30,6 +30,8 @@ pub enum ConnectionError {
SocketPath(#[from] SocketPathError),
#[error(transparent)]
Io(#[from] io::Error),
#[error("connection timed out")]
Timeout,
}
#[derive(Debug, Error)]
@@ -57,6 +59,7 @@ pub enum IpcError {
pub const DEFAULT_PORT: u16 = 4242;
#[derive(Debug, Default, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Position {
#[default]
Left,
@@ -199,10 +202,21 @@ pub enum FrontendEvent {
AuthorizedUpdated(HashMap<String, String>),
/// public key fingerprint of this device
PublicKeyFingerprint(String),
/// incoming connected
IncomingConnected(String, SocketAddr, Position),
/// new device connected
DeviceConnected {
addr: SocketAddr,
fingerprint: String,
},
/// incoming device entered the screen
DeviceEntered {
fingerprint: String,
addr: SocketAddr,
pos: Position,
},
/// incoming disconnected
IncomingDisconnected(SocketAddr),
/// failed connection attempt (approval for fingerprint required)
ConnectionAttempt { fingerprint: String },
}
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
@@ -237,6 +251,8 @@ pub enum FrontendRequest {
AuthorizeKey(String, String),
/// remove fingerprint (fingerprint)
RemoveAuthorizedKey(String),
/// change the hook command
UpdateEnterHook(u64, Option<String>),
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]

View File

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

View File

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

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!"

View File

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

View File

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

View File

@@ -1,12 +1,16 @@
use crate::config::Config;
use clap::Args;
use futures::StreamExt;
use input_capture::{self, CaptureError, CaptureEvent, InputCapture, InputCaptureError, Position};
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!("creating input capture");
let backend = config.capture_backend.map(|b| b.into());
let backend = config.capture_backend().map(|b| b.into());
loop {
let mut input_capture = InputCapture::new(backend).await?;
log::info!("creating clients");

View File

@@ -199,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
pub(crate) fn set_resolving(&self, handle: ClientHandle, status: bool) {
if let Some((_, s)) = self.clients.borrow_mut().get_mut(handle as usize) {

View File

@@ -1,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 std::collections::HashMap;
use std::env::{self, VarError};
@@ -10,7 +12,8 @@ use std::{collections::HashSet, io};
use thiserror::Error;
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::{
self,
@@ -21,33 +24,50 @@ use shadow_rs::shadow;
shadow!(build);
#[derive(Serialize, Deserialize, Debug)]
pub struct ConfigToml {
pub capture_backend: Option<CaptureBackend>,
pub emulation_backend: Option<EmulationBackend>,
pub port: Option<u16>,
pub frontend: Option<Frontend>,
pub release_bind: Option<Vec<scancode::Linux>>,
pub cert_path: Option<PathBuf>,
pub left: Option<TomlClient>,
pub right: Option<TomlClient>,
pub top: Option<TomlClient>,
pub bottom: Option<TomlClient>,
pub authorized_fingerprints: Option<HashMap<String, String>>,
const CONFIG_FILE_NAME: &str = "config.toml";
const CERT_FILE_NAME: &str = "lan-mouse.pem";
fn default_path() -> Result<PathBuf, VarError> {
#[cfg(unix)]
let default_path = {
let xdg_config_home =
env::var("XDG_CONFIG_HOME").unwrap_or(format!("{}/.config", env::var("HOME")?));
format!("{xdg_config_home}/lan-mouse/")
};
#[cfg(not(unix))]
let default_path = {
let app_data =
env::var("LOCALAPPDATA").unwrap_or(format!("{}/.config", env::var("USERPROFILE")?));
format!("{app_data}\\lan-mouse\\")
};
Ok(PathBuf::from(default_path))
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct TomlClient {
pub hostname: Option<String>,
pub host_name: Option<String>,
pub ips: Option<Vec<IpAddr>>,
pub port: Option<u16>,
pub activate_on_startup: Option<bool>,
pub enter_hook: Option<String>,
#[derive(Serialize, Deserialize, Debug)]
struct ConfigToml {
capture_backend: Option<CaptureBackend>,
emulation_backend: Option<EmulationBackend>,
port: Option<u16>,
release_bind: Option<Vec<scancode::Linux>>,
cert_path: Option<PathBuf>,
clients: Option<Vec<TomlClient>>,
authorized_fingerprints: Option<HashMap<String, String>>,
}
#[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 {
pub fn new(path: &Path) -> Result<ConfigToml, ConfigError> {
fn new(path: &Path) -> Result<ConfigToml, ConfigError> {
let config = fs::read_to_string(path)?;
Ok(toml::from_str::<_>(&config)?)
}
@@ -55,30 +75,14 @@ impl ConfigToml {
#[derive(Parser, Debug)]
#[command(author, version=build::CLAP_LONG_VERSION, about, long_about = None)]
struct CliArgs {
struct Args {
/// the listen port for lan-mouse
#[arg(short, long)]
port: Option<u16>,
/// the frontend to use [cli | gtk]
#[arg(short, long)]
frontend: Option<Frontend>,
/// non-default config file location
#[arg(short, long)]
config: Option<String>,
/// run only the service as a daemon without the frontend
#[arg(short, long)]
daemon: bool,
/// test input capture
#[arg(long)]
test_capture: bool,
/// test input emulation
#[arg(long)]
test_emulation: bool,
config: Option<PathBuf>,
/// capture backend override
#[arg(long)]
@@ -91,6 +95,22 @@ struct CliArgs {
/// path to non-default certificate location
#[arg(long)]
cert_path: Option<PathBuf>,
/// subcommands
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Subcommand, Clone, Debug, Eq, PartialEq)]
pub enum Command {
/// test input emulation
TestEmulation(TestEmulationArgs),
/// test input capture
TestCapture(TestCaptureArgs),
/// Lan Mouse commandline interface
Cli(CliArgs),
/// run in daemon mode
Daemon,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
@@ -214,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)]
pub struct Config {
/// the path to the configuration file used
pub path: PathBuf,
/// public key fingerprints authorized for connection
pub authorized_fingerprints: HashMap<String, String>,
/// optional input-capture backend override
pub capture_backend: Option<CaptureBackend>,
/// optional input-emulation backend override
pub emulation_backend: Option<EmulationBackend>,
/// the frontend to use
pub frontend: Frontend,
/// the port to use (initially)
pub port: u16,
/// list of clients
pub clients: Vec<(TomlClient, Position)>,
/// whether or not to run as a daemon
pub daemon: bool,
/// configured release bind
pub release_bind: Vec<scancode::Linux>,
/// test capture instead of running the app
pub test_capture: bool,
/// test emulation instead of running the app
pub test_emulation: bool,
/// path to the tls certificate to use
pub cert_path: PathBuf,
/// command line arguments
args: Args,
/// path to the certificate file used
cert_path: PathBuf,
/// path to the config file used
config_path: PathBuf,
/// the (optional) toml config and it's path
config_toml: Option<ConfigToml>,
}
pub struct ConfigClient {
@@ -269,6 +255,25 @@ pub struct ConfigClient {
pub enter_hook: Option<String>,
}
impl From<TomlClient> for ConfigClient {
fn from(toml: TomlClient) -> Self {
let active = toml.activate_on_startup.unwrap_or(false);
let enter_hook = toml.enter_hook;
let hostname = toml.hostname;
let ips = HashSet::from_iter(toml.ips.into_iter().flatten());
let port = toml.port.unwrap_or(DEFAULT_PORT);
let pos = toml.position.unwrap_or_default();
Self {
ips,
hostname,
port,
pos,
active,
enter_hook,
}
}
}
#[derive(Debug, Error)]
pub enum ConfigError {
#[error(transparent)]
@@ -284,134 +289,99 @@ const DEFAULT_RELEASE_KEYS: [scancode::Linux; 4] =
impl Config {
pub fn new() -> Result<Self, ConfigError> {
let args = CliArgs::parse();
const CONFIG_FILE_NAME: &str = "config.toml";
const CERT_FILE_NAME: &str = "lan-mouse.pem";
#[cfg(unix)]
let config_path = {
let xdg_config_home =
env::var("XDG_CONFIG_HOME").unwrap_or(format!("{}/.config", env::var("HOME")?));
format!("{xdg_config_home}/lan-mouse/")
};
#[cfg(not(unix))]
let config_path = {
let app_data =
env::var("LOCALAPPDATA").unwrap_or(format!("{}/.config", env::var("USERPROFILE")?));
format!("{app_data}\\lan-mouse\\")
};
let config_path = PathBuf::from(config_path);
let config_file = config_path.join(CONFIG_FILE_NAME);
let args = Args::parse();
// --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) => {
log::warn!("{config_file:?}: {e}");
log::warn!("{config_path:?}: {e}");
log::warn!("Continuing without config file ...");
None
}
Ok(c) => Some(c),
};
let frontend_arg = args.frontend;
let frontend_cfg = config_toml.as_ref().and_then(|c| c.frontend);
let frontend = frontend_arg.or(frontend_cfg).unwrap_or_default();
let port = args
.port
.or(config_toml.as_ref().and_then(|c| c.port))
.unwrap_or(DEFAULT_PORT);
log::debug!("{config_toml:?}");
let release_bind = config_toml
.as_ref()
.and_then(|c| c.release_bind.clone())
.unwrap_or(Vec::from_iter(DEFAULT_RELEASE_KEYS.iter().cloned()));
let capture_backend = args
.capture_backend
.or(config_toml.as_ref().and_then(|c| c.capture_backend));
let emulation_backend = args
.emulation_backend
.or(config_toml.as_ref().and_then(|c| c.emulation_backend));
// --cert-path <file> overrules default location
let cert_path = args
.cert_path
.clone()
.or(config_toml.as_ref().and_then(|c| c.cert_path.clone()))
.unwrap_or(config_path.join(CERT_FILE_NAME));
let authorized_fingerprints = config_toml
.as_mut()
.and_then(|c| std::mem::take(&mut c.authorized_fingerprints))
.unwrap_or_default();
let mut clients: Vec<(TomlClient, Position)> = vec![];
if let Some(config_toml) = config_toml {
if let Some(c) = config_toml.right {
clients.push((c, Position::Right))
}
if let Some(c) = config_toml.left {
clients.push((c, Position::Left))
}
if let Some(c) = config_toml.top {
clients.push((c, Position::Top))
}
if let Some(c) = config_toml.bottom {
clients.push((c, Position::Bottom))
}
}
let daemon = args.daemon;
let test_capture = args.test_capture;
let test_emulation = args.test_emulation;
.unwrap_or(default_path()?.join(CERT_FILE_NAME));
Ok(Config {
path: config_path,
authorized_fingerprints,
capture_backend,
emulation_backend,
daemon,
frontend,
clients,
port,
release_bind,
test_capture,
test_emulation,
args,
cert_path,
config_path,
config_toml,
})
}
pub fn get_clients(&self) -> Vec<ConfigClient> {
self.clients
.iter()
.map(|(c, pos)| {
let port = c.port.unwrap_or(DEFAULT_PORT);
let ips: HashSet<IpAddr> = if let Some(ips) = c.ips.as_ref() {
HashSet::from_iter(ips.iter().cloned())
} else {
HashSet::new()
};
let hostname = match &c.hostname {
Some(h) => Some(h.clone()),
None => c.host_name.clone(),
};
let active = c.activate_on_startup.unwrap_or(false);
let enter_hook = c.enter_hook.clone();
ConfigClient {
ips,
hostname,
port,
pos: *pos,
active,
enter_hook,
}
})
/// the command to run
pub fn command(&self) -> Option<Command> {
self.args.command.clone()
}
pub fn config_path(&self) -> &Path {
&self.config_path
}
/// public key fingerprints authorized for connection
pub fn authorized_fingerprints(&self) -> HashMap<String, String> {
self.config_toml
.as_ref()
.and_then(|c| c.authorized_fingerprints.clone())
.unwrap_or_default()
}
/// path to certificate
pub fn cert_path(&self) -> &Path {
&self.cert_path
}
/// optional input-capture backend override
pub fn capture_backend(&self) -> Option<CaptureBackend> {
self.args
.capture_backend
.or(self.config_toml.as_ref().and_then(|c| c.capture_backend))
}
/// optional input-emulation backend override
pub fn emulation_backend(&self) -> Option<EmulationBackend> {
self.args
.emulation_backend
.or(self.config_toml.as_ref().and_then(|c| c.emulation_backend))
}
/// the port to use (initially)
pub fn port(&self) -> u16 {
self.args
.port
.or(self.config_toml.as_ref().and_then(|c| c.port))
.unwrap_or(DEFAULT_PORT)
}
/// list of configured clients
pub fn clients(&self) -> Vec<ConfigClient> {
self.config_toml
.as_ref()
.map(|c| c.clients.clone())
.unwrap_or_default()
.into_iter()
.flatten()
.map(From::<TomlClient>::from)
.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 lan_mouse_ipc::{ClientHandle, DEFAULT_PORT};
use lan_mouse_proto::{ProtoEvent, MAX_EVENT_SIZE};
use local_channel::mpsc::{channel, Receiver, Sender};
use lan_mouse_proto::{MAX_EVENT_SIZE, ProtoEvent};
use local_channel::mpsc::{Receiver, Sender, channel};
use std::{
cell::RefCell,
collections::{HashMap, HashSet},
@@ -15,7 +15,7 @@ use thiserror::Error;
use tokio::{
net::UdpSocket,
sync::Mutex,
task::{spawn_local, JoinSet},
task::{JoinSet, spawn_local},
};
use webrtc_dtls::{
config::{Config, ExtendedMasterSecretType},
@@ -223,14 +223,18 @@ async fn ping_pong(
) {
loop {
let (buf, len) = ProtoEvent::Ping.into();
if let Err(e) = conn.send(&buf[..len]).await {
log::warn!("{addr}: send error `{e}`, closing connection");
let _ = conn.close().await;
break;
}
log::trace!("PING >->->->->- {addr}");
tokio::time::sleep(Duration::from_millis(500)).await;
// send 4 pings, at least one must be answered
for _ in 0..4 {
if let Err(e) = conn.send(&buf[..len]).await {
log::warn!("{addr}: send error `{e}`, closing connection");
let _ = conn.close().await;
break;
}
log::trace!("PING >->->->->- {addr}");
tokio::time::sleep(Duration::from_millis(500)).await;
}
if !ping_response.borrow_mut().remove(&addr) {
log::warn!("{addr} did not respond, closing connection");

View File

@@ -1,9 +1,9 @@
use std::{collections::HashMap, net::IpAddr};
use local_channel::mpsc::{channel, Receiver, Sender};
use tokio::task::{spawn_local, JoinHandle};
use local_channel::mpsc::{Receiver, Sender, channel};
use tokio::task::{JoinHandle, spawn_local};
use hickory_resolver::{error::ResolveError, TokioAsyncResolver};
use hickory_resolver::{ResolveError, TokioResolver};
use tokio_util::sync::CancellationToken;
use lan_mouse_ipc::ClientHandle;
@@ -26,7 +26,7 @@ pub(crate) enum DnsEvent {
}
struct DnsTask {
resolver: TokioAsyncResolver,
resolver: TokioResolver,
request_rx: Receiver<DnsRequest>,
event_tx: Sender<DnsEvent>,
cancellation_token: CancellationToken,
@@ -35,7 +35,7 @@ struct DnsTask {
impl DnsResolver {
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 (event_tx, event_rx) = channel();
let cancellation_token = CancellationToken::new();

View File

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

View File

@@ -1,4 +1,5 @@
use crate::config::Config;
use clap::Args;
use input_emulation::{InputEmulation, InputEmulationError};
use input_event::{Event, PointerEvent};
use std::f64::consts::PI;
@@ -7,10 +8,20 @@ use std::time::{Duration, Instant};
const FREQUENCY_HZ: f64 = 1.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");
let backend = config.emulation_backend.map(|b| b.into());
let backend = config.emulation_backend().map(|b| b.into());
let mut emulation = InputEmulation::new(backend).await?;
emulation.create(0).await;

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ use crate::{
listen::{LanMouseListener, ListenerCreationError},
};
use futures::StreamExt;
use hickory_resolver::error::ResolveError;
use hickory_resolver::ResolveError;
use lan_mouse_ipc::{
AsyncFrontendListener, ClientConfig, ClientHandle, ClientState, FrontendEvent, FrontendRequest,
IpcError, IpcListenerCreationError, Position, Status,
@@ -80,7 +80,7 @@ struct Incoming {
impl Service {
pub async fn new(config: Config) -> Result<Self, ServiceError> {
let client_manager = ClientManager::default();
for client in config.get_clients() {
for client in config.clients() {
let config = ClientConfig {
hostname: client.hostname,
fix_ips: client.ips.into_iter().collect(),
@@ -99,28 +99,28 @@ impl Service {
}
// load certificate
let cert = crypto::load_or_generate_key_and_cert(&config.cert_path)?;
let cert = crypto::load_or_generate_key_and_cert(config.cert_path())?;
let public_key_fingerprint = crypto::certificate_fingerprint(&cert);
// create frontend communication adapter, exit if already running
let frontend_listener = AsyncFrontendListener::new().await?;
let authorized_keys = Arc::new(RwLock::new(config.authorized_fingerprints.clone()));
let authorized_keys = Arc::new(RwLock::new(config.authorized_fingerprints()));
// listener + connection
let listener =
LanMouseListener::new(config.port, cert.clone(), authorized_keys.clone()).await?;
LanMouseListener::new(config.port(), cert.clone(), authorized_keys.clone()).await?;
let conn = LanMouseConnection::new(cert.clone(), client_manager.clone());
// input capture + emulation
let capture_backend = config.capture_backend.map(|b| b.into());
let capture = Capture::new(capture_backend, conn, config.release_bind.clone());
let emulation_backend = config.emulation_backend.map(|b| b.into());
let capture_backend = config.capture_backend().map(|b| b.into());
let capture = Capture::new(capture_backend, conn, config.release_bind());
let emulation_backend = config.emulation_backend().map(|b| b.into());
let emulation = Emulation::new(emulation_backend, listener);
// create dns resolver
let resolver = DnsResolver::new()?;
let port = config.port;
let port = config.port();
let service = Self {
capture,
emulation,
@@ -142,11 +142,15 @@ impl Service {
}
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
// is already active in client_manager and does not create a
// capture barrier in that case so we have to deactivate it first
self.client_manager.deactivate_client(handle);
self.client_manager.deactivate_client(*handle);
}
for handle in active {
self.activate_client(handle);
}
@@ -193,6 +197,9 @@ impl Service {
FrontendRequest::ResolveDns(handle) => self.resolve(handle),
FrontendRequest::Sync => self.sync_frontend(),
FrontendRequest::RemoveAuthorizedKey(key) => self.remove_authorized_key(key),
FrontendRequest::UpdateEnterHook(handle, enter_hook) => {
self.update_enter_hook(handle, enter_hook)
}
}
}
@@ -204,7 +211,10 @@ impl Service {
fn handle_emulation_event(&mut self, event: EmulationEvent) {
match event {
EmulationEvent::Connected {
EmulationEvent::ConnectionAttempt { fingerprint } => {
self.notify_frontend(FrontendEvent::ConnectionAttempt { fingerprint });
}
EmulationEvent::Entered {
addr,
pos,
fingerprint,
@@ -212,7 +222,11 @@ impl Service {
// check if already registered
if !self.incoming_conns.contains(&addr) {
self.add_incoming(addr, pos, fingerprint.clone());
self.notify_frontend(FrontendEvent::IncomingConnected(fingerprint, addr, pos));
self.notify_frontend(FrontendEvent::DeviceEntered {
fingerprint,
addr,
pos,
});
} else {
self.update_incoming(addr, pos, fingerprint);
}
@@ -239,6 +253,9 @@ impl Service {
self.notify_frontend(FrontendEvent::EmulationStatus(self.emulation_status));
}
EmulationEvent::ReleaseNotify => self.capture.release(),
EmulationEvent::Connected { addr, fingerprint } => {
self.notify_frontend(FrontendEvent::DeviceConnected { addr, fingerprint });
}
}
}
@@ -340,7 +357,11 @@ impl Service {
self.remove_incoming(addr);
self.add_incoming(addr, pos, fingerprint.clone());
self.notify_frontend(FrontendEvent::IncomingDisconnected(addr));
self.notify_frontend(FrontendEvent::IncomingConnected(fingerprint, addr, pos));
self.notify_frontend(FrontendEvent::DeviceEntered {
fingerprint,
addr,
pos,
});
}
}
@@ -476,6 +497,11 @@ impl Service {
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) {
let event = self
.client_manager