Compare commits

...

12 Commits

Author SHA1 Message Date
Ferdinand Schober
d49ba36b34 prevent authorization request spamming windows 2025-10-27 23:39:27 +01:00
Ferdinand Schober
4e5a66340a Partially Revert "slow scrolling chrome with emulation=wlroots capture=layer-shell (#318) (#325)" (#334)
The division by 120 was correct.
2025-10-27 15:56:30 +01:00
Micah R Ledbetter
0a0d91b0da Include libadwaita and other dependencies in the app bundle on macOS (#271)
* Include libadwaita and other dependencies in the app bundle on macOS

* Fix missing pipes

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

* Use correct path for homebrew bash on Intel macOS

* Get homebrew path from the brew command

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

Remove need for recent bash altogether

* Fix permissions nit

* Update macOS dylib copy script path in release workflow

* fix a few typos

* fix script invocation in pre-release.yml

---------

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

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

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

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

Using the lan-mouse shows:

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

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

* update windows

* clippy: inline format args

* update flake

* update core-graphics

* fix poll after completion error

* fix ashpd?!
2025-10-08 16:10:32 +02:00
Ferdinand Schober
9f10ebcbd2 Macos cleanup event thread (#324) 2025-10-08 02:00:37 +02:00
Ferdinand Schober
e29eb7134c macos: fix a crash when InputCapture is dropped (#323) 2025-10-08 00:22:52 +02:00
Ferdinand Schober
e46fe60b3e fix parent class types in key_row widget (#300)
closes #294
2025-06-12 18:17:36 +02:00
Ferdinand Schober
37a4e236b8 fix clippy warnings from rust 1.87 (#301) 2025-06-12 17:52:23 +02:00
33 changed files with 1413 additions and 1077 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

2055
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -81,15 +81,37 @@ paru -S lan-mouse-git
- flake: [README.md](./nix/README.md)
</details>
<details>
<summary>Fedora</summary>
You can install Lan Mouse from the [Terra Repository](https://terra.fyralabs.com).
After enabling Terra:
```sh
dnf install lan-mouse
```
</details>
<details>
<summary>MacOS</summary>
- Download the package for your Mac (Intel or ARM) from the releases page
- Unzip it
- Remove the quarantine with `xattr -rd com.apple.quarantine "Lan Mouse.app"`
- Launch the app
- Grant accessibility permissions in System Preferences
</details>
<details>
<summary>Manual Installation</summary>
First make sure to [install the necessary dependencies](#installing-dependencies).
First make sure to [install the necessary dependencies](#installing-dependencies-for-development--compiling-from-source).
Precompiled release binaries for Windows, MacOS and Linux are available in the [releases section](https://github.com/feschber/lan-mouse/releases).
For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies).
For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies-for-development--compiling-from-source).
Alternatively, the `lan-mouse` binary can be compiled from source (see below).
@@ -161,7 +183,15 @@ For a detailed list of available features, checkout the [Cargo.toml](./Cargo.tom
<summary>MacOS</summary>
```sh
brew install libadwaita pkg-config
# Install dependencies
brew install libadwaita pkg-config imagemagick
cargo install cargo-bundle
# Create the macOS icon file
scripts/makeicns.sh
# Create the .app bundle
cargo bundle
# Copy all dynamic libraries into the bundle, and update the bundle to find them there
scripts/copy-macos-dylib.sh
```
</details>

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

12
flake.lock generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -161,12 +161,12 @@ fn get_display_at_point(x: CGFloat, y: CGFloat) -> Option<CGDirectDisplayID> {
};
if error != 0 {
log::warn!("error getting displays at point ({}, {}): {}", x, y, error);
log::warn!("error getting displays at point ({x}, {y}): {error}");
return Option::None;
}
if display_count == 0 {
log::debug!("no displays found at point ({}, {})", x, y);
log::debug!("no displays found at point ({x}, {y})");
return Option::None;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -324,7 +324,7 @@ impl Window {
pub(super) fn update_client_config(&self, handle: ClientHandle, client: ClientConfig) {
let Some(row) = self.row_for_handle(handle) else {
log::warn!("could not find row for handle {}", handle);
log::warn!("could not find row for handle {handle}");
return;
};
row.set_hostname(client.hostname);
@@ -334,11 +334,11 @@ impl Window {
pub(super) fn update_client_state(&self, handle: ClientHandle, state: ClientState) {
let Some(row) = self.row_for_handle(handle) else {
log::warn!("could not find row for handle {}", handle);
log::warn!("could not find row for handle {handle}");
return;
};
let Some(client_object) = self.client_object_for_handle(handle) else {
log::warn!("could not find row for handle {}", handle);
log::warn!("could not find row for handle {handle}");
return;
};
@@ -474,6 +474,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 +499,6 @@ impl Window {
}),
);
window.present();
self.imp().authorization_window.replace(Some(window));
}
}

View File

@@ -8,6 +8,8 @@ use gtk::{gdk, gio, glib, Button, CompositeTemplate, Entry, Image, Label, ListBo
use lan_mouse_ipc::{FrontendRequestWriter, DEFAULT_PORT};
use crate::authorization_window::AuthorizationWindow;
#[derive(CompositeTemplate, Default)]
#[template(resource = "/de/feschber/LanMouse/window.ui")]
pub struct Window {
@@ -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

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

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

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

View File

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

@@ -264,7 +264,7 @@ async fn read_loop(
}
}
}
log::info!("dtls client disconnected {:?}", addr);
log::info!("dtls client disconnected {addr:?}");
let mut conns = conns.lock().await;
let index = conns
.iter()

View File

@@ -9,7 +9,7 @@ use crate::{
listen::{LanMouseListener, ListenerCreationError},
};
use futures::StreamExt;
use hickory_resolver::error::ResolveError;
use hickory_resolver::ResolveError;
use lan_mouse_ipc::{
AsyncFrontendListener, ClientConfig, ClientHandle, ClientState, FrontendEvent, FrontendRequest,
IpcError, IpcListenerCreationError, Position, Status,