Compare commits

..

16 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
46 changed files with 631 additions and 362 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

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>

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

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 {

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

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

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, CallbackResult, 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,7 +418,7 @@ fn create_event_tap<'a>(
}
}
if let Some(pos) = pos {
if let Some(pos) = capture_position {
res_events.iter().for_each(|e| {
// error must be ignored, since the event channel
// may already be closed when the InputCapture instance is dropped.
@@ -493,10 +523,7 @@ impl MacOSInputCapture {
log::error!("Failed to handle producer event: {e}");
})
}
_ = &mut tap_exit_rx => {
break;
}
_ = &mut tap_exit_rx => break,
}
}
// show cursor

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 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, HOOKPROC,
KBDLLHOOKSTRUCT, LLKHF_EXTENDED, MSG, MSLLHOOKSTRUCT, WH_KEYBOARD_LL, WH_MOUSE_LL,
WINDOW_STYLE, WM_DISPLAYCHANGE, WM_KEYDOWN, WM_KEYUP, WM_LBUTTONDOWN, WM_LBUTTONUP,
WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_RBUTTONDOWN,
WM_RBUTTONUP, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_USER, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSW,
WNDPROC,
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>>>,

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"

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

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,

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

View File

@@ -5,8 +5,8 @@ use std::{net::IpAddr, time::Duration};
use thiserror::Error;
use lan_mouse_ipc::{
connect_async, ClientHandle, ConnectionError, FrontendEvent, FrontendRequest, IpcError,
Position,
ClientHandle, ConnectionError, FrontendEvent, FrontendRequest, IpcError, Position,
connect_async,
};
#[derive(Debug, Error)]

View File

@@ -4,8 +4,9 @@ use adw::prelude::*;
use adw::subclass::prelude::*;
use glib::subclass::InitializingObject;
use gtk::{
Button, CompositeTemplate, Label,
glib::{self, subclass::Signal},
template_callbacks, Button, CompositeTemplate, Label,
template_callbacks,
};
#[derive(CompositeTemplate, Default)]

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

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

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

@@ -13,7 +13,7 @@ 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;

View File

@@ -4,16 +4,15 @@ 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::{
@@ -474,6 +473,9 @@ impl Window {
}
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(
@@ -496,5 +498,6 @@ impl Window {
}),
);
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]

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,7 +1,7 @@
use crate::{ConnectionError, FrontendEvent, FrontendRequest, IpcError};
use std::{
cmp::min,
task::{ready, Poll},
task::{Poll, ready},
time::Duration,
};

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)]

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

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

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

@@ -13,7 +13,7 @@ use thiserror::Error;
use toml;
use lan_mouse_cli::CliArgs;
use lan_mouse_ipc::{Position, DEFAULT_PORT};
use lan_mouse_ipc::{DEFAULT_PORT, Position};
use input_event::scancode::{
self,

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,7 +1,7 @@
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::{ResolveError, TokioResolver};
use tokio_util::sync::CancellationToken;

View File

@@ -3,7 +3,7 @@ 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
@@ -128,6 +128,7 @@ 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() => {match e {
@@ -156,7 +157,10 @@ impl ListenTask {
self.event_tx.send(EmulationEvent::Connected { addr, fingerprint }).expect("channel closed");
}
Some(ListenEvent::Rejected { fingerprint }) => {
self.event_tx.send(EmulationEvent::ConnectionAttempt { fingerprint }).expect("channel closed");
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
}}

View File

@@ -1,6 +1,6 @@
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, VecDeque},
@@ -12,7 +12,7 @@ use std::{
use thiserror::Error;
use tokio::{
sync::Mutex as AsyncMutex,
task::{spawn_local, JoinHandle},
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;