Compare commits

..

1 Commits

Author SHA1 Message Date
Ferdinand Schober
3ee49753c0 simplify configuration 2025-03-15 18:22:31 +01:00
42 changed files with 1234 additions and 1946 deletions

View File

@@ -94,9 +94,7 @@ jobs:
- name: Install cargo bundle - name: Install cargo bundle
run: cargo install cargo-bundle run: cargo install cargo-bundle
- name: Bundle - name: Bundle
run: | run: cargo bundle --release
cargo bundle --release
scripts/copy-macos-dylib.sh "target/release/bundle/osx/Lan Mouse.app/Contents/MacOS/lan-mouse"
- name: Zip bundle - name: Zip bundle
run: | run: |
cd target/release/bundle/osx cd target/release/bundle/osx
@@ -122,9 +120,7 @@ jobs:
- name: Install cargo bundle - name: Install cargo bundle
run: cargo install cargo-bundle run: cargo install cargo-bundle
- name: Bundle - name: Bundle
run: | run: cargo bundle --release
cargo bundle --release
scripts/copy-macos-dylib.sh "target/release/bundle/osx/Lan Mouse.app/Contents/MacOS/lan-mouse"
- name: Zip bundle - name: Zip bundle
run: | run: |
cd target/release/bundle/osx cd target/release/bundle/osx

View File

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

View File

@@ -90,9 +90,7 @@ jobs:
- name: Install cargo bundle - name: Install cargo bundle
run: cargo install cargo-bundle run: cargo install cargo-bundle
- name: Bundle - name: Bundle
run: | run: cargo bundle --release
cargo bundle --release
scripts/copy-macos-dylib.sh "target/release/bundle/osx/Lan Mouse.app/Contents/MacOS/lan-mouse"
- name: Zip bundle - name: Zip bundle
run: | run: |
cd target/release/bundle/osx cd target/release/bundle/osx
@@ -118,9 +116,7 @@ jobs:
- name: Install cargo bundle - name: Install cargo bundle
run: cargo install cargo-bundle run: cargo install cargo-bundle
- name: Bundle - name: Bundle
run: | run: cargo bundle --release
cargo bundle --release
scripts/copy-macos-dylib.sh "target/release/bundle/osx/Lan Mouse.app/Contents/MacOS/lan-mouse"
- name: Zip bundle - name: Zip bundle
run: | run: |
cd target/release/bundle/osx cd target/release/bundle/osx

4
.gitignore vendored
View File

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

2061
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

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

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

1
dylibs/.gitignore vendored
View File

@@ -1 +0,0 @@
*

12
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1752687322, "lastModified": 1740560979,
"narHash": "sha256-RKwfXA4OZROjBTQAl9WOZQFm7L8Bo93FQwSJpAiSRvo=", "narHash": "sha256-Vr3Qi346M+8CjedtbyUevIGDZW8LcA1fTG0ugPY/Hic=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "6e987485eb2c77e5dcc5af4e3c70843711ef9251", "rev": "5135c59491985879812717f4c9fea69604e7f26f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -29,11 +29,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1752806774, "lastModified": 1740623427,
"narHash": "sha256-4cHeoR2roN7d/3J6gT+l6o7J2hTrBIUiCwVdDNMeXzE=", "narHash": "sha256-3SdPQrZoa4odlScFDUHd4CUPQ/R1gtH4Mq9u8CBiK8M=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "3c90219b3ba1c9790c45a078eae121de48a39c55", "rev": "d342e8b5fd88421ff982f383c853f0fc78a847ab",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

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

View File

@@ -535,7 +535,7 @@ impl State {
fn update_windows(&mut self) { fn update_windows(&mut self) {
log::info!("active outputs: "); log::info!("active outputs: ");
for output in self.outputs.iter().filter(|o| o.info.is_some()) { for output in self.outputs.iter().filter(|o| o.info.is_some()) {
log::info!(" * {output}"); log::info!(" * {}", output);
} }
self.active_windows.clear(); self.active_windows.clear();
@@ -582,17 +582,17 @@ impl Inner {
match self.queue.dispatch_pending(&mut self.state) { match self.queue.dispatch_pending(&mut self.state) {
Ok(_) => {} Ok(_) => {}
Err(DispatchError::Backend(WaylandError::Io(e))) => { Err(DispatchError::Backend(WaylandError::Io(e))) => {
log::error!("Wayland Error: {e}"); log::error!("Wayland Error: {}", e);
} }
Err(DispatchError::Backend(e)) => { Err(DispatchError::Backend(e)) => {
panic!("backend error: {e}"); panic!("backend error: {}", e);
} }
Err(DispatchError::BadMessage { Err(DispatchError::BadMessage {
sender_id, sender_id,
interface, interface,
opcode, opcode,
}) => { }) => {
panic!("bad message {sender_id}, {interface} , {opcode}"); panic!("bad message {}, {} , {}", sender_id, interface, opcode);
} }
} }
} }
@@ -813,7 +813,7 @@ impl Dispatch<WlPointer, ()> for State {
})), })),
)); ));
} }
wl_pointer::Event::Frame => { wl_pointer::Event::Frame {} => {
// TODO properly handle frame events // TODO properly handle frame events
// we simply insert a frame event on the client side // we simply insert a frame event on the client side
// after each event for now // after each event for now
@@ -974,7 +974,7 @@ impl Dispatch<ZxdgOutputV1, u32> for State {
.find(|o| o.global.name == *name) .find(|o| o.global.name == *name)
.expect("output"); .expect("output");
log::debug!("xdg_output {name} - {event:?}"); log::debug!("xdg_output {name} - {:?}", event);
match event { match event {
zxdg_output_v1::Event::LogicalPosition { x, y } => { zxdg_output_v1::Event::LogicalPosition { x, y } => {
output.pending_info.position = (x, y); output.pending_info.position = (x, y);
@@ -1010,7 +1010,7 @@ impl Dispatch<WlOutput, u32> for State {
_conn: &Connection, _conn: &Connection,
_qhandle: &QueueHandle<Self>, _qhandle: &QueueHandle<Self>,
) { ) {
log::debug!("wl_output {name} - {event:?}"); log::debug!("wl_output {name} - {:?}", event);
if let wl_output::Event::Done = event { if let wl_output::Event::Done = event {
state.update_output_info(*name); state.update_output_info(*name);
} }

View File

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

View File

@@ -587,13 +587,9 @@ impl LanMouseInputCapture for LibeiInputCapture<'_> {
self.cancellation_token.cancel(); self.cancellation_token.cancel();
let task = &mut self.capture_task; let task = &mut self.capture_task;
log::debug!("waiting for capture to terminate..."); log::debug!("waiting for capture to terminate...");
let res = if !task.is_finished() { let res = task.await.expect("libei task panic");
task.await.expect("libei task panic")
} else {
Ok(())
};
self.terminated = true;
log::debug!("done!"); log::debug!("done!");
self.terminated = true;
res res
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,7 @@ use wayland_client::backend::WaylandError;
use wayland_client::WEnum; use wayland_client::WEnum;
use wayland_client::protocol::wl_keyboard::{self, WlKeyboard}; use wayland_client::protocol::wl_keyboard::{self, WlKeyboard};
use wayland_client::protocol::wl_pointer::{Axis, AxisSource, ButtonState}; use wayland_client::protocol::wl_pointer::{Axis, ButtonState};
use wayland_client::protocol::wl_seat::WlSeat; use wayland_client::protocol::wl_seat::WlSeat;
use wayland_protocols_wlr::virtual_pointer::v1::client::{ use wayland_protocols_wlr::virtual_pointer::v1::client::{
zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1 as VpManager, zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1 as VpManager,
@@ -163,13 +163,13 @@ impl Emulation for WlrootsEmulation {
async fn create(&mut self, handle: EmulationHandle) { async fn create(&mut self, handle: EmulationHandle) {
self.state.add_client(handle); self.state.add_client(handle);
if let Err(e) = self.queue.flush() { if let Err(e) = self.queue.flush() {
log::error!("{e}"); log::error!("{}", e);
} }
} }
async fn destroy(&mut self, handle: EmulationHandle) { async fn destroy(&mut self, handle: EmulationHandle) {
self.state.destroy_client(handle); self.state.destroy_client(handle);
if let Err(e) = self.queue.flush() { if let Err(e) = self.queue.flush() {
log::error!("{e}"); log::error!("{}", e);
} }
} }
async fn terminate(&mut self) { async fn terminate(&mut self) {
@@ -210,8 +210,7 @@ impl VirtualInput {
PointerEvent::AxisDiscrete120 { axis, value } => { PointerEvent::AxisDiscrete120 { axis, value } => {
let axis: Axis = (axis as u32).try_into()?; let axis: Axis = (axis as u32).try_into()?;
self.pointer self.pointer
.axis_discrete(now, axis, value as f64 / 8., value / 120); .axis_discrete(now, axis, value as f64 / 6., value / 120);
self.pointer.axis_source(AxisSource::Wheel);
self.pointer.frame(); self.pointer.frame();
} }
} }
@@ -222,7 +221,7 @@ impl VirtualInput {
self.keyboard.key(time, key, state as u32); self.keyboard.key(time, key, state as u32);
if let Ok(mut mods) = self.modifiers.lock() { if let Ok(mut mods) = self.modifiers.lock() {
if mods.update_by_key_event(key, state) { if mods.update_by_key_event(key, state) {
log::trace!("Key triggers modifier change: {mods:?}"); log::trace!("Key triggers modifier change: {:?}", mods);
self.keyboard.modifiers( self.keyboard.modifiers(
mods.mask_pressed().bits(), mods.mask_pressed().bits(),
0, 0,
@@ -331,7 +330,7 @@ impl XMods {
fn update_by_key_event(&mut self, key: u32, state: u8) -> bool { fn update_by_key_event(&mut self, key: u32, state: u8) -> bool {
if let Ok(key) = scancode::Linux::try_from(key) { if let Ok(key) = scancode::Linux::try_from(key) {
log::trace!("Attempting to process modifier from: {key:#?}"); log::trace!("Attempting to process modifier from: {:#?}", key);
let pressed_mask = match key { let pressed_mask = match key {
scancode::Linux::KeyLeftShift | scancode::Linux::KeyRightShift => XMods::ShiftMask, scancode::Linux::KeyLeftShift | scancode::Linux::KeyRightShift => XMods::ShiftMask,
scancode::Linux::KeyLeftCtrl | scancode::Linux::KeyRightCtrl => XMods::ControlMask, scancode::Linux::KeyLeftCtrl | scancode::Linux::KeyRightCtrl => XMods::ControlMask,
@@ -349,7 +348,7 @@ impl XMods {
// unchanged // unchanged
if pressed_mask.is_empty() && locked_mask.is_empty() { if pressed_mask.is_empty() && locked_mask.is_empty() {
log::trace!("{key:#?} is not a modifier key"); log::trace!("{:#?} is not a modifier key", key);
return false; return false;
} }
match state { match state {

View File

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

View File

@@ -143,6 +143,7 @@ impl Emulation for DesktopPortalEmulation<'_> {
impl AsyncDrop for DesktopPortalEmulation<'_> { impl AsyncDrop for DesktopPortalEmulation<'_> {
#[doc = r" Perform the async cleanup."] #[doc = r" Perform the async cleanup."]
#[must_use]
#[allow(clippy::type_complexity, clippy::type_repetition_in_bounds)] #[allow(clippy::type_complexity, clippy::type_repetition_in_bounds)]
fn async_drop<'async_trait>( fn async_drop<'async_trait>(
self, self,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
mod authorization_window;
mod client_object; mod client_object;
mod client_row; mod client_row;
mod fingerprint_window; mod fingerprint_window;
@@ -147,21 +146,8 @@ fn build_ui(app: &Application) {
FrontendEvent::EmulationStatus(s) => window.set_emulation(s.into()), FrontendEvent::EmulationStatus(s) => window.set_emulation(s.into()),
FrontendEvent::AuthorizedUpdated(keys) => window.set_authorized_keys(keys), FrontendEvent::AuthorizedUpdated(keys) => window.set_authorized_keys(keys),
FrontendEvent::PublicKeyFingerprint(fp) => window.set_pk_fp(&fp), FrontendEvent::PublicKeyFingerprint(fp) => window.set_pk_fp(&fp),
FrontendEvent::ConnectionAttempt { fingerprint } => { FrontendEvent::IncomingConnected(_fingerprint, addr, pos) => {
window.request_authorization(&fingerprint); window.show_toast(format!("device connected: {addr} ({pos})").as_str());
}
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) => { FrontendEvent::IncomingDisconnected(addr) => {
window.show_toast(format!("{addr} disconnected").as_str()); window.show_toast(format!("{addr} disconnected").as_str());

View File

@@ -16,10 +16,7 @@ use lan_mouse_ipc::{
DEFAULT_PORT, DEFAULT_PORT,
}; };
use crate::{ use crate::{fingerprint_window::FingerprintWindow, key_object::KeyObject, key_row::KeyRow};
authorization_window::AuthorizationWindow, fingerprint_window::FingerprintWindow,
key_object::KeyObject, key_row::KeyRow,
};
use super::{client_object::ClientObject, client_row::ClientRow}; use super::{client_object::ClientObject, client_row::ClientRow};
@@ -324,7 +321,7 @@ impl Window {
pub(super) fn update_client_config(&self, handle: ClientHandle, client: ClientConfig) { pub(super) fn update_client_config(&self, handle: ClientHandle, client: ClientConfig) {
let Some(row) = self.row_for_handle(handle) else { 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; return;
}; };
row.set_hostname(client.hostname); row.set_hostname(client.hostname);
@@ -334,11 +331,11 @@ impl Window {
pub(super) fn update_client_state(&self, handle: ClientHandle, state: ClientState) { pub(super) fn update_client_state(&self, handle: ClientHandle, state: ClientState) {
let Some(row) = self.row_for_handle(handle) else { 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; return;
}; };
let Some(client_object) = self.client_object_for_handle(handle) else { 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; return;
}; };
@@ -397,8 +394,8 @@ impl Window {
self.request(FrontendRequest::Create); self.request(FrontendRequest::Create);
} }
fn open_fingerprint_dialog(&self, fp: Option<String>) { fn open_fingerprint_dialog(&self) {
let window = FingerprintWindow::new(fp); let window = FingerprintWindow::new();
window.set_transient_for(Some(self)); window.set_transient_for(Some(self));
window.connect_closure( window.connect_closure(
"confirm-clicked", "confirm-clicked",
@@ -472,33 +469,4 @@ impl Window {
pub(super) fn set_pk_fp(&self, fingerprint: &str) { pub(super) fn set_pk_fp(&self, fingerprint: &str) {
self.imp().fingerprint_row.set_subtitle(fingerprint); self.imp().fingerprint_row.set_subtitle(fingerprint);
} }
pub(super) fn request_authorization(&self, fingerprint: &str) {
if let Some(w) = self.imp().authorization_window.borrow_mut().take() {
w.close();
}
let window = AuthorizationWindow::new(fingerprint);
window.set_transient_for(Some(self));
window.connect_closure(
"confirm-clicked",
false,
closure_local!(
#[strong(rename_to = parent)]
self,
move |w: AuthorizationWindow, fp: String| {
w.close();
parent.open_fingerprint_dialog(Some(fp));
}
),
);
window.connect_closure(
"cancel-clicked",
false,
closure_local!(move |w: AuthorizationWindow| {
w.close();
}),
);
window.present();
self.imp().authorization_window.replace(Some(window));
}
} }

View File

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

View File

@@ -59,7 +59,6 @@ pub enum IpcError {
pub const DEFAULT_PORT: u16 = 4242; pub const DEFAULT_PORT: u16 = 4242;
#[derive(Debug, Default, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)] #[derive(Debug, Default, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Position { pub enum Position {
#[default] #[default]
Left, Left,
@@ -202,21 +201,10 @@ pub enum FrontendEvent {
AuthorizedUpdated(HashMap<String, String>), AuthorizedUpdated(HashMap<String, String>),
/// public key fingerprint of this device /// public key fingerprint of this device
PublicKeyFingerprint(String), PublicKeyFingerprint(String),
/// new device connected /// incoming connected
DeviceConnected { IncomingConnected(String, SocketAddr, Position),
addr: SocketAddr,
fingerprint: String,
},
/// incoming device entered the screen
DeviceEntered {
fingerprint: String,
addr: SocketAddr,
pos: Position,
},
/// incoming disconnected /// incoming disconnected
IncomingDisconnected(SocketAddr), IncomingDisconnected(SocketAddr),
/// failed connection attempt (approval for fingerprint required)
ConnectionAttempt { fingerprint: String },
} }
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]

View File

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

View File

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

View File

@@ -51,7 +51,7 @@ struct ConfigToml {
port: Option<u16>, port: Option<u16>,
release_bind: Option<Vec<scancode::Linux>>, release_bind: Option<Vec<scancode::Linux>>,
cert_path: Option<PathBuf>, cert_path: Option<PathBuf>,
clients: Option<Vec<TomlClient>>, clients: Vec<TomlClient>,
authorized_fingerprints: Option<HashMap<String, String>>, authorized_fingerprints: Option<HashMap<String, String>>,
} }
@@ -61,7 +61,7 @@ struct TomlClient {
host_name: Option<String>, host_name: Option<String>,
ips: Option<Vec<IpAddr>>, ips: Option<Vec<IpAddr>>,
port: Option<u16>, port: Option<u16>,
position: Option<Position>, pos: Option<Position>,
activate_on_startup: Option<bool>, activate_on_startup: Option<bool>,
enter_hook: Option<String>, enter_hook: Option<String>,
} }
@@ -262,7 +262,7 @@ impl From<TomlClient> for ConfigClient {
let hostname = toml.hostname; let hostname = toml.hostname;
let ips = HashSet::from_iter(toml.ips.into_iter().flatten()); let ips = HashSet::from_iter(toml.ips.into_iter().flatten());
let port = toml.port.unwrap_or(DEFAULT_PORT); let port = toml.port.unwrap_or(DEFAULT_PORT);
let pos = toml.position.unwrap_or_default(); let pos = toml.pos.unwrap_or_default();
Self { Self {
ips, ips,
hostname, hostname,
@@ -370,7 +370,6 @@ impl Config {
self.config_toml self.config_toml
.as_ref() .as_ref()
.map(|c| c.clients.clone()) .map(|c| c.clients.clone())
.unwrap_or_default()
.into_iter() .into_iter()
.flatten() .flatten()
.map(From::<TomlClient>::from) .map(From::<TomlClient>::from)

View File

@@ -3,7 +3,7 @@ use std::{collections::HashMap, net::IpAddr};
use local_channel::mpsc::{channel, Receiver, Sender}; use local_channel::mpsc::{channel, Receiver, Sender};
use tokio::task::{spawn_local, JoinHandle}; use tokio::task::{spawn_local, JoinHandle};
use hickory_resolver::{ResolveError, TokioResolver}; use hickory_resolver::{error::ResolveError, TokioAsyncResolver};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use lan_mouse_ipc::ClientHandle; use lan_mouse_ipc::ClientHandle;
@@ -26,7 +26,7 @@ pub(crate) enum DnsEvent {
} }
struct DnsTask { struct DnsTask {
resolver: TokioResolver, resolver: TokioAsyncResolver,
request_rx: Receiver<DnsRequest>, request_rx: Receiver<DnsRequest>,
event_tx: Sender<DnsEvent>, event_tx: Sender<DnsEvent>,
cancellation_token: CancellationToken, cancellation_token: CancellationToken,
@@ -35,7 +35,7 @@ struct DnsTask {
impl DnsResolver { impl DnsResolver {
pub(crate) fn new() -> Result<Self, ResolveError> { pub(crate) fn new() -> Result<Self, ResolveError> {
let resolver = TokioResolver::builder_tokio()?.build(); let resolver = TokioAsyncResolver::tokio_from_system_conf()?;
let (request_tx, request_rx) = channel(); let (request_tx, request_rx) = channel();
let (event_tx, event_rx) = channel(); let (event_tx, event_rx) = channel();
let cancellation_token = CancellationToken::new(); let cancellation_token = CancellationToken::new();

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ use crate::{
listen::{LanMouseListener, ListenerCreationError}, listen::{LanMouseListener, ListenerCreationError},
}; };
use futures::StreamExt; use futures::StreamExt;
use hickory_resolver::ResolveError; use hickory_resolver::error::ResolveError;
use lan_mouse_ipc::{ use lan_mouse_ipc::{
AsyncFrontendListener, ClientConfig, ClientHandle, ClientState, FrontendEvent, FrontendRequest, AsyncFrontendListener, ClientConfig, ClientHandle, ClientState, FrontendEvent, FrontendRequest,
IpcError, IpcListenerCreationError, Position, Status, IpcError, IpcListenerCreationError, Position, Status,
@@ -211,10 +211,7 @@ impl Service {
fn handle_emulation_event(&mut self, event: EmulationEvent) { fn handle_emulation_event(&mut self, event: EmulationEvent) {
match event { match event {
EmulationEvent::ConnectionAttempt { fingerprint } => { EmulationEvent::Connected {
self.notify_frontend(FrontendEvent::ConnectionAttempt { fingerprint });
}
EmulationEvent::Entered {
addr, addr,
pos, pos,
fingerprint, fingerprint,
@@ -222,11 +219,7 @@ impl Service {
// check if already registered // check if already registered
if !self.incoming_conns.contains(&addr) { if !self.incoming_conns.contains(&addr) {
self.add_incoming(addr, pos, fingerprint.clone()); self.add_incoming(addr, pos, fingerprint.clone());
self.notify_frontend(FrontendEvent::DeviceEntered { self.notify_frontend(FrontendEvent::IncomingConnected(fingerprint, addr, pos));
fingerprint,
addr,
pos,
});
} else { } else {
self.update_incoming(addr, pos, fingerprint); self.update_incoming(addr, pos, fingerprint);
} }
@@ -253,9 +246,6 @@ impl Service {
self.notify_frontend(FrontendEvent::EmulationStatus(self.emulation_status)); self.notify_frontend(FrontendEvent::EmulationStatus(self.emulation_status));
} }
EmulationEvent::ReleaseNotify => self.capture.release(), EmulationEvent::ReleaseNotify => self.capture.release(),
EmulationEvent::Connected { addr, fingerprint } => {
self.notify_frontend(FrontendEvent::DeviceConnected { addr, fingerprint });
}
} }
} }
@@ -357,11 +347,7 @@ impl Service {
self.remove_incoming(addr); self.remove_incoming(addr);
self.add_incoming(addr, pos, fingerprint.clone()); self.add_incoming(addr, pos, fingerprint.clone());
self.notify_frontend(FrontendEvent::IncomingDisconnected(addr)); self.notify_frontend(FrontendEvent::IncomingDisconnected(addr));
self.notify_frontend(FrontendEvent::DeviceEntered { self.notify_frontend(FrontendEvent::IncomingConnected(fingerprint, addr, pos));
fingerprint,
addr,
pos,
});
} }
} }