Compare commits

...

13 Commits

Author SHA1 Message Date
mrcha033
6b1eddef7a fix(libei): only restart the capture session on device change under GNOME
`EiEvent::DeviceRemoved` currently tears down and recreates the whole
input-capture session. That is a workaround for mutter, which stops
delivering events after a device change, but it is applied on every
compositor.

Elsewhere the restart is pure overhead: every device change costs a
CreateSession + ConnectToEIS round trip. On compositors that keep
per-session state around it is worse than overhead - Hyprland keeps a
keymap fd per EIS session and never reaps sessions whose client went
away, so the churn drives it out of file descriptors and eventually
crashes it. On this machine the churn ran at ~35 ConnectToEIS per 38
minutes; gating it to GNOME brought that to 0 with no change in capture
behaviour.

`SeatRemoved` still releases unconditionally - a seat going away really
does invalidate the session.

The default is derived from XDG_CURRENT_DESKTOP and can be overridden
with LM_RESTART_SESSION_ON_DEVICE_CHANGE=1/0 for compositors that need
the mutter behaviour but do not advertise themselves as GNOME.
2026-08-31 14:11:10 +02:00
Ian Douglas Scott
09889cd839 chore: Minor code style improvements to Wayland code 2026-08-31 12:59:27 +02:00
Ian Douglas Scott
c194df791d fix(input-capture/layer_shell): Call read() on guard only once
With `wayland-backend/client_system` enabled, this was causing an
infinite loop since `read` never returned `WouldBlock`.

This seems to be an inconsistency in wayland-rs:
https://github.com/Smithay/wayland-rs/pull/959

In any case, it isn't necessary to call `read` more than once.
2026-08-31 12:59:27 +02:00
Ferdinand Schober
b316af6a8f fix vs-version 2026-08-31 10:51:17 +02:00
Ferdinand Schober
d09a574380 tell gvsbuild to target newer VisualStudio version 2026-08-31 10:44:58 +02:00
Ferdinand Schober
b115208aad fix clippy lint 2026-08-31 10:34:02 +02:00
Ty Smith
392af44cbe fix(ci): bust stale Homebrew glib cache on macOS runners
The Swatinem/rust-cache action was restored before brew installed the
native dependencies. When Homebrew updated glib (2.88.0 → newer) the
cached Rust build artifacts still referenced the old versioned Cellar
path (/opt/homebrew/Cellar/glib/2.88.0/lib), causing a linker failure:

    ld: library 'gio-2.0' not found

Fix by:
1. Moving `brew install` before the rust-cache step so libs are current
   before the cache is consulted.
2. Capturing `brew list --versions glib gtk4 libadwaita` into a
   prefix-key so the cache key changes whenever those packages update,
   forcing a clean Rust build with fresh pkg-config paths.

Linux and Windows cache behaviour is unchanged (MACOS_LIB_VER unset →
empty prefix-key → same default v0- prefix).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-28 16:22:39 +02:00
Ferdinand Schober
133e1ae81d hotfix: plasma 6.7 reporting Barrier ID 1
Still not 100% sure of the root cause, but it looks
like plasma is reporting 1 as the barrier id in all cases...
2026-06-21 19:06:25 +02:00
Ian Douglas Scott
6fb7e24746 Update reis to 0.7.0 2026-06-20 01:17:02 +02:00
Ferdinand Schober
d1f41180e3 include .deb and .AppImage in pre-release as well 2026-06-15 12:00:40 +02:00
Ferdinand Schober
497a1a081a adjust paths to deb / appimage 2026-06-15 01:01:38 +02:00
Ferdinand Schober
5342b475ae add deb and AppImage to release workflow 2026-06-15 00:41:43 +02:00
Jon Kinney
a42592ab05 fix(dns): resolve hostnames via the OS resolver instead of pure DNS
hickory_resolver's TokioResolver only consults /etc/resolv.conf and
queries upstream DNS servers — which means it can't see /etc/hosts,
mDNS (Avahi/Bonjour), NetBIOS, or anything else in the system's full
name-resolution stack. On a typical home LAN there's no DNS server
that knows about peer machine names, so users had to fall back to
typing IP addresses, which broke the moment they moved their setup
to a different network.

Swap to tokio::net::lookup_host, which calls getaddrinfo (or
GetAddrInfoEx on Windows). That walks /etc/nsswitch.conf on Linux
(picking up Avahi-resolved .local names, /etc/hosts, and DNS), uses
Bonjour for .local on macOS, and the full Windows resolver on
Windows. A Bonjour hostname like "JKMBP-M4-Max.local" now resolves
on every modern network without explicit configuration; the user
can carry their two machines between LANs and the connection still
finds them.

Drop the hickory-resolver dependency entirely — it's no longer
needed. ServiceError::Dns also goes away; lookup failures surface as
io::Error which is already covered by ServiceError::Io.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-14 23:31:48 +02:00
13 changed files with 156 additions and 308 deletions

View File

@@ -30,11 +30,19 @@ jobs:
run: |
cargo build --release
cp target/release/lan-mouse lan-mouse-linux-x86_64
- name: Install cargo bundle
run: cargo install cargo-bundle
- name: Bundle
run: |
cargo bundle --release
- name: Upload build artifact
uses: actions/upload-artifact@v6
with:
name: lan-mouse-linux-x86_64
path: lan-mouse-linux-x86_64
path: |
lan-mouse-linux-x86_64
target/release/bundle/deb/lan-mouse_*.deb
target/release/bundle/appimage/lan-mouse_*.AppImage
linux-arm64-release-build:
runs-on: ubuntu-22.04-arm
@@ -49,11 +57,19 @@ jobs:
run: |
cargo build --release
cp target/release/lan-mouse lan-mouse-linux-arm64
- name: Install cargo bundle
run: cargo install cargo-bundle
- name: Bundle
run: |
cargo bundle --release
- name: Upload build artifact
uses: actions/upload-artifact@v6
with:
name: lan-mouse-linux-arm64
path: lan-mouse-linux-arm64
path: |
lan-mouse-linux-arm64
target/release/bundle/deb/lan-mouse_*.deb
target/release/bundle/appimage/lan-mouse_*.AppImage
windows-release-build:
runs-on: windows-latest
@@ -89,7 +105,7 @@ jobs:
# see https://github.com/wingtk/gvsbuild/pull/1004
Move-Item "C:\Program Files\Git\usr\bin" "C:\Program Files\Git\usr\notbin"
Move-Item "C:\Program Files\Git\bin" "C:\Program Files\Git\notbin"
gvsbuild build --msys-dir=C:\msys64 gtk4 libadwaita librsvg
gvsbuild build --vs-ver=vs2026 --msys-dir=C:\msys64 gtk4 libadwaita librsvg
Move-Item "C:\Program Files\Git\usr\notbin" "C:\Program Files\Git\usr\bin"
Move-Item "C:\Program Files\Git\notbin" "C:\Program Files\Git\bin"
- uses: actions/checkout@v6
@@ -188,7 +204,11 @@ jobs:
generate_release_notes: true
files: |
lan-mouse-linux-x86_64/lan-mouse-linux-x86_64
lan-mouse-linux-x86_64/target/release/bundle/deb/lan-mouse_*.deb
lan-mouse-linux-x86_64/target/release/bundle/appimage/lan-mouse_*.AppImage
lan-mouse-linux-arm64/lan-mouse-linux-arm64
lan-mouse-linux-arm64/target/release/bundle/deb/lan-mouse_*.deb
lan-mouse-linux-arm64/target/release/bundle/appimage/lan-mouse_*.AppImage
lan-mouse-macos-intel/lan-mouse-macos-intel.zip
lan-mouse-macos-arm64/lan-mouse-macos-arm64.zip
lan-mouse-windows-x86_64/lan-mouse-windows-x86_64.zip
@@ -201,7 +221,11 @@ jobs:
generate_release_notes: true
files: |
lan-mouse-linux-x86_64/lan-mouse-linux-x86_64
lan-mouse-linux-x86_64/target/release/bundle/deb/lan-mouse_*.deb
lan-mouse-linux-x86_64/target/release/bundle/appimage/lan-mouse_*.AppImage
lan-mouse-linux-arm64/lan-mouse-linux-arm64
lan-mouse-linux-arm64/target/release/bundle/deb/lan-mouse_*.deb
lan-mouse-linux-arm64/target/release/bundle/appimage/lan-mouse_*.AppImage
lan-mouse-macos-intel/lan-mouse-macos-intel.zip
lan-mouse-macos-arm64/lan-mouse-macos-arm64.zip
lan-mouse-windows-x86_64/lan-mouse-windows-x86_64.zip

View File

@@ -40,7 +40,6 @@ jobs:
- test
steps:
- uses: actions/checkout@v6
- uses: Swatinem/rust-cache@v2
- name: Install Linux deps
if: runner.os == 'Linux'
run: |
@@ -49,6 +48,13 @@ jobs:
- name: Install macOS dependencies
if: runner.os == 'macOS'
run: brew install gtk4 libadwaita imagemagick
- name: Record macOS native lib versions for cache key
if: runner.os == 'macOS'
run: |
echo "MACOS_LIB_VER=$(brew list --versions glib gtk4 libadwaita | tr '\n' '_')" >> "$GITHUB_ENV"
- uses: Swatinem/rust-cache@v2
with:
prefix-key: ${{ env.MACOS_LIB_VER }}
- name: Install Windows Dependencies - create gtk dir
if: runner.os == 'Windows'
run: mkdir C:\gtk-build\gtk\x64\release
@@ -77,7 +83,7 @@ jobs:
py -m venv .venv
.venv\Scripts\activate.ps1
py -m pip install gvsbuild
gvsbuild build --msys-dir=C:\msys64 gtk4 libadwaita librsvg
gvsbuild build --vs-ver=vs2026 --msys-dir=C:\msys64 gtk4 libadwaita librsvg
- name: cargo build
if: matrix.job == 'build'
run: cargo build

256
Cargo.lock generated
View File

@@ -549,30 +549,6 @@ dependencies = [
"libc",
]
[[package]]
name = "critical-section"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
@@ -586,7 +562,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
dependencies = [
"generic-array",
"rand_core 0.6.4",
"rand_core",
"subtle",
"zeroize",
]
@@ -598,7 +574,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"rand_core 0.6.4",
"rand_core",
"typenum",
]
@@ -735,7 +711,7 @@ dependencies = [
"hkdf",
"pem-rfc7468",
"pkcs8",
"rand_core 0.6.4",
"rand_core",
"sec1",
"subtle",
"zeroize",
@@ -747,18 +723,6 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
[[package]]
name = "enum-as-inner"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "enumflags2"
version = "0.7.12"
@@ -852,7 +816,7 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
dependencies = [
"rand_core 0.6.4",
"rand_core",
"subtle",
]
@@ -1281,7 +1245,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
dependencies = [
"ff",
"rand_core 0.6.4",
"rand_core",
"subtle",
]
@@ -1395,52 +1359,6 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hickory-proto"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502"
dependencies = [
"async-trait",
"cfg-if",
"data-encoding",
"enum-as-inner",
"futures-channel",
"futures-io",
"futures-util",
"idna",
"ipnet",
"once_cell",
"rand 0.9.2",
"ring",
"thiserror 2.0.18",
"tinyvec",
"tokio",
"tracing",
"url",
]
[[package]]
name = "hickory-resolver"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a"
dependencies = [
"cfg-if",
"futures-util",
"hickory-proto",
"ipconfig",
"moka",
"once_cell",
"parking_lot",
"rand 0.9.2",
"resolv-conf",
"smallvec",
"thiserror 2.0.18",
"tokio",
"tracing",
]
[[package]]
name = "hkdf"
version = "0.12.4"
@@ -1713,19 +1631,6 @@ dependencies = [
"thiserror 2.0.18",
]
[[package]]
name = "ipconfig"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222"
dependencies = [
"socket2",
"widestring",
"windows-registry",
"windows-result 0.4.1",
"windows-sys 0.61.2",
]
[[package]]
name = "ipnet"
version = "2.12.0"
@@ -1845,7 +1750,6 @@ dependencies = [
"clap",
"env_logger",
"futures",
"hickory-resolver",
"input-capture",
"input-emulation",
"input-event",
@@ -1994,12 +1898,6 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
@@ -2096,23 +1994,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "moka"
version = "0.12.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046"
dependencies = [
"crossbeam-channel",
"crossbeam-epoch",
"crossbeam-utils",
"equivalent",
"parking_lot",
"portable-atomic",
"smallvec",
"tagptr",
"uuid",
]
[[package]]
name = "nix"
version = "0.26.4"
@@ -2242,10 +2123,6 @@ name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
dependencies = [
"critical-section",
"portable-atomic",
]
[[package]]
name = "once_cell_polyfill"
@@ -2530,18 +2407,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.5",
"rand_chacha",
"rand_core",
]
[[package]]
@@ -2551,17 +2418,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.5",
"rand_core",
]
[[package]]
@@ -2573,15 +2430,6 @@ dependencies = [
"getrandom 0.2.17",
]
[[package]]
name = "rand_core"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "rcgen"
version = "0.13.2"
@@ -2635,21 +2483,17 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "reis"
version = "0.5.0"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00939c5c526a1b4054ef8d9d96b3f92227f08ca355965e986741b556eda6d289"
checksum = "81f3fedd2777cde52c1be5e572efbec485eac7b801c47820eda388d4f13b9c4b"
dependencies = [
"futures",
"rustix 0.38.44",
"enumflags2",
"futures-util",
"log",
"rustix",
"tokio",
]
[[package]]
name = "resolv-conf"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7"
[[package]]
name = "rfc6979"
version = "0.4.0"
@@ -2692,19 +2536,6 @@ dependencies = [
"nom",
]
[[package]]
name = "rustix"
version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
"bitflags 2.11.0",
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.59.0",
]
[[package]]
name = "rustix"
version = "1.1.4"
@@ -2714,7 +2545,7 @@ dependencies = [
"bitflags 2.11.0",
"errno",
"libc",
"linux-raw-sys 0.12.1",
"linux-raw-sys",
"windows-sys 0.61.2",
]
@@ -2929,7 +2760,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest",
"rand_core 0.6.4",
"rand_core",
]
[[package]]
@@ -3017,12 +2848,6 @@ dependencies = [
"version-compare",
]
[[package]]
name = "tagptr"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
[[package]]
name = "target-lexicon"
version = "0.13.3"
@@ -3038,7 +2863,7 @@ dependencies = [
"fastrand",
"getrandom 0.4.2",
"once_cell",
"rustix 1.1.4",
"rustix",
"windows-sys 0.61.2",
]
@@ -3125,21 +2950,6 @@ dependencies = [
"zerovec",
]
[[package]]
name = "tinyvec"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.51.1"
@@ -3417,7 +3227,6 @@ version = "1.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
dependencies = [
"getrandom 0.4.2",
"js-sys",
"serde_core",
"wasm-bindgen",
@@ -3562,7 +3371,7 @@ checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d"
dependencies = [
"cc",
"downcast-rs",
"rustix 1.1.4",
"rustix",
"smallvec",
"wayland-sys",
]
@@ -3574,7 +3383,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144"
dependencies = [
"bitflags 2.11.0",
"rustix 1.1.4",
"rustix",
"wayland-backend",
"wayland-scanner",
]
@@ -3658,8 +3467,8 @@ dependencies = [
"p384",
"pem",
"portable-atomic",
"rand 0.8.5",
"rand_core 0.6.4",
"rand",
"rand_core",
"rcgen",
"ring",
"rustls",
@@ -3690,18 +3499,12 @@ dependencies = [
"log",
"nix",
"portable-atomic",
"rand 0.8.5",
"rand",
"thiserror 1.0.69",
"tokio",
"winapi",
]
[[package]]
name = "widestring"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
[[package]]
name = "winapi"
version = "0.3.9"
@@ -3836,17 +3639,6 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-registry"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
dependencies = [
"windows-link 0.2.1",
"windows-result 0.4.1",
"windows-strings 0.5.1",
]
[[package]]
name = "windows-result"
version = "0.3.4"
@@ -4186,7 +3978,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
dependencies = [
"curve25519-dalek",
"rand_core 0.6.4",
"rand_core",
"serde",
"zeroize",
]
@@ -4256,7 +4048,7 @@ dependencies = [
"hex",
"libc",
"ordered-stream",
"rustix 1.1.4",
"rustix",
"serde",
"serde_repr",
"tokio",

View File

@@ -36,7 +36,6 @@ lan-mouse-ipc = { path = "lan-mouse-ipc", version = "0.3.0" }
lan-mouse-proto = { path = "lan-mouse-proto", version = "0.3.0" }
shadow-rs = { version = "1.2.0", features = ["metadata"] }
hickory-resolver = "0.25.2"
toml = "0.8"
toml_edit = { version = "0.22", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }
@@ -94,7 +93,8 @@ rdp_emulation = ["input-emulation/remote_desktop_portal"]
[package.metadata.bundle]
name = "Lan Mouse"
icon = ["target/icon.icns"]
icon = ["target/icon.icns", "lan-mouse-gtk/resources/de.feschber.LanMouse.svg"]
identifier = "de.feschber.LanMouse"
deb_depends = [ "libadwaita-1-0" ]
osx_info_plist_exts = ["build-aux/macos-lsui-element.plist"]
resources = ["target/menubar-template.png"]

View File

@@ -45,7 +45,7 @@ ashpd = { version = "0.13.9", default-features = false, features = [
"input_capture",
"tokio",
], optional = true }
reis = { version = "0.5.0", features = ["tokio"], optional = true }
reis = { version = "0.7.0", features = ["tokio"], optional = true }
[target.'cfg(target_os="macos")'.dependencies]
core-graphics = { version = "0.25.0", features = ["highsierra"] }

View File

@@ -123,7 +123,6 @@ struct State {
focused: Option<Arc<Window>>,
global_list: GlobalList,
globals: Globals,
wayland_fd: RawFd,
read_guard: Option<ReadEventsGuard>,
qh: QueueHandle<Self>,
pending_events: VecDeque<(Position, CaptureEvent)>,
@@ -138,7 +137,7 @@ struct Inner {
impl AsRawFd for Inner {
fn as_raw_fd(&self) -> RawFd {
self.state.wayland_fd
self.queue.as_fd().as_raw_fd()
}
}
@@ -332,7 +331,6 @@ impl LayerShellInputCapture {
active_windows: Vec::new(),
focused: None,
qh,
wayland_fd: queue.as_fd().as_raw_fd(),
read_guard: None,
pending_events: VecDeque::new(),
outputs: vec![],
@@ -548,13 +546,12 @@ impl State {
}
impl Inner {
fn read(&mut self) -> bool {
fn read(&mut self) {
match self.state.read_guard.take().unwrap().read() {
Ok(_) => true,
Err(WaylandError::Io(e)) if e.kind() == ErrorKind::WouldBlock => false,
Ok(_) => {}
Err(WaylandError::Io(e)) if e.kind() == ErrorKind::WouldBlock => {}
Err(WaylandError::Io(e)) => {
log::error!("error reading from wayland socket: {e}");
false
}
Err(WaylandError::Protocol(e)) => {
panic!("wayland protocol violation: {e}")
@@ -564,16 +561,11 @@ impl Inner {
fn prepare_read(&mut self) -> io::Result<()> {
loop {
match self.queue.prepare_read() {
None => match self.queue.dispatch_pending(&mut self.state) {
Ok(_) => continue,
Err(DispatchError::Backend(WaylandError::Io(e))) => return Err(e),
Err(e) => panic!("failed to dispatch wayland events: {e}"),
},
Some(r) => {
self.state.read_guard = Some(r);
break Ok(());
}
if let Some(guard) = self.queue.prepare_read() {
self.state.read_guard = Some(guard);
break Ok(());
} else {
self.dispatch_events();
}
}
}
@@ -655,13 +647,7 @@ impl Stream for LayerShellInputCapture {
let inner = guard.get_inner_mut();
// read events
while inner.read() {
// prepare next read
match inner.prepare_read() {
Ok(_) => {}
Err(e) => return Poll::Ready(Some(Err(e.into()))),
}
}
inner.read();
// dispatch the events
inner.dispatch_events();

View File

@@ -18,12 +18,12 @@ use reis::{
use std::{
cell::Cell,
collections::HashMap,
io,
env, io,
num::NonZeroU32,
os::unix::net::UnixStream,
pin::Pin,
rc::Rc,
sync::Arc,
sync::{Arc, LazyLock},
task::{Context, Poll},
};
use tokio::{
@@ -50,6 +50,25 @@ use super::{
* prevents receiving further events after a session has been disabled once.
* Therefore the session needs to be recreated when the barriers are updated */
/* mutter also kills the session whenever ei devices come and go, so there the
* whole session has to be torn down and recreated on every device change.
* Elsewhere that is pure overhead: each restart costs a CreateSession +
* ConnectToEIS round trip, and compositors that keep per-session state around
* (hyprland leaks a keymap fd per eis session, see hyprwm/Hyprland) can be
* driven out of file descriptors by the churn.
* Set LM_RESTART_SESSION_ON_DEVICE_CHANGE=1/0 to override the default. */
static RESTART_SESSION_ON_DEVICE_CHANGE: LazyLock<bool> =
LazyLock::new(restart_session_on_device_change);
fn restart_session_on_device_change() -> bool {
match env::var("LM_RESTART_SESSION_ON_DEVICE_CHANGE").as_deref() {
Ok("1") => true,
Ok("0") => false,
_ => env::var("XDG_CURRENT_DESKTOP")
.is_ok_and(|desktops| desktops.to_uppercase().split(':').any(|d| d == "GNOME")),
}
}
/// events that necessitate restarting the capture session
#[derive(Clone, Copy, Debug)]
enum LibeiNotifyEvent {
@@ -413,7 +432,15 @@ async fn do_capture_session(
};
// find client corresponding to barrier
let pos = *pos_for_barrier_id.get(&barrier_id).expect("invalid barrier id");
let pos = match pos_for_barrier_id.get(&barrier_id) {
Some(id) => *id,
None => {
log::warn!("INVALID BARRIER ID: Id {barrier_id} does not exist!");
let id = find_corresponding_client(&barriers, activated.cursor_position().expect("no cursor position reported by compositor"));
let pos = *pos_for_barrier_id.get(&id).expect("invalid barrier id");
pos
},
};
current_pos.replace(Some(pos));
// client entered => send event
@@ -529,15 +556,6 @@ fn distance_to_line(line: ((f32, f32), (f32, f32)), p: (f32, f32)) -> f32 {
distance
}
static ALL_CAPABILITIES: &[DeviceCapability] = &[
DeviceCapability::Pointer,
DeviceCapability::PointerAbsolute,
DeviceCapability::Keyboard,
DeviceCapability::Touch,
DeviceCapability::Scroll,
DeviceCapability::Button,
];
async fn handle_ei_event(
ei_event: EiEvent,
current_client: Option<Position>,
@@ -545,25 +563,43 @@ async fn handle_ei_event(
event_tx: &Sender<(Position, CaptureEvent)>,
release_session: &Notify,
) -> Result<(), CaptureError> {
let all_capabilities = DeviceCapability::Pointer
| DeviceCapability::PointerAbsolute
| DeviceCapability::Keyboard
| DeviceCapability::Touch
| DeviceCapability::Scroll
| DeviceCapability::Button;
match ei_event {
EiEvent::SeatAdded(s) => {
s.seat.bind_capabilities(ALL_CAPABILITIES);
s.seat.bind_capabilities(all_capabilities);
context.flush().map_err(|e| io::Error::new(e.kind(), e))?;
}
EiEvent::SeatRemoved(_) | /* EiEvent::DeviceAdded(_) | */ EiEvent::DeviceRemoved(_) => {
EiEvent::SeatRemoved(_) => {
log::debug!("releasing session: {ei_event:?}");
release_session.notify_waiters();
}
/* EiEvent::DeviceAdded(_) | */
EiEvent::DeviceRemoved(_) => {
if *RESTART_SESSION_ON_DEVICE_CHANGE {
log::debug!("releasing session: {ei_event:?}");
release_session.notify_waiters();
} else {
log::debug!("ignoring device change: {ei_event:?}");
}
}
EiEvent::DevicePaused(_) | EiEvent::DeviceResumed(_) => {}
EiEvent::DeviceStartEmulating(_) => log::debug!("START EMULATING"),
EiEvent::DeviceStopEmulating(_) => log::debug!("STOP EMULATING"),
EiEvent::Disconnected(d) => {
return Err(CaptureError::Disconnected(format!("{:?}", d.reason)))
return Err(CaptureError::Disconnected(format!("{:?}", d.reason)));
}
_ => {
if let Some(pos) = current_client {
for event in Event::from_ei_event(ei_event) {
event_tx.send((pos, CaptureEvent::Input(event))).await.expect("no channel");
event_tx
.send((pos, CaptureEvent::Input(event)))
.await
.expect("no channel");
}
}
}

View File

@@ -45,7 +45,7 @@ ashpd = { version = "0.13.9", default-features = false, features = [
"screencast",
"tokio",
], optional = true }
reis = { version = "0.5.0", features = ["tokio"], optional = true }
reis = { version = "0.7.0", features = ["tokio"], optional = true }
[target.'cfg(target_os="macos")'.dependencies]
bitflags = "2.6.0"

View File

@@ -291,14 +291,12 @@ async fn ei_event_handler(
) -> Result<(), EmulationError> {
loop {
let event = events.next().await.ok_or(EmulationError::EndOfStream)??;
const CAPABILITIES: &[DeviceCapability] = &[
DeviceCapability::Pointer,
DeviceCapability::PointerAbsolute,
DeviceCapability::Keyboard,
DeviceCapability::Touch,
DeviceCapability::Scroll,
DeviceCapability::Button,
];
let capabilities = DeviceCapability::Pointer
| DeviceCapability::PointerAbsolute
| DeviceCapability::Keyboard
| DeviceCapability::Touch
| DeviceCapability::Scroll
| DeviceCapability::Button;
log::debug!("{event:?}");
match event {
EiEvent::Disconnected(e) => {
@@ -306,7 +304,7 @@ async fn ei_event_handler(
return Err(EmulationError::EndOfStream);
}
EiEvent::SeatAdded(e) => {
e.seat().bind_capabilities(CAPABILITIES);
e.seat().bind_capabilities(capabilities);
}
EiEvent::SeatRemoved(e) => {
log::debug!("seat removed: {:?}", e.seat());
@@ -314,7 +312,6 @@ async fn ei_event_handler(
EiEvent::DeviceAdded(e) => {
let device_type = e.device().device_type();
log::debug!("device added: {device_type:?}");
e.device().device();
let device = e.device();
if let Some(pointer) = e.device().interface::<Pointer>() {
devices

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.5.0", optional = true }
reis = { version = "0.7.0", optional = true }
[features]
default = ["libei"]

View File

@@ -518,7 +518,7 @@ impl Config {
}
pub fn read_from_disk(&mut self) -> Result<bool, io::Error> {
log::info!("reading config from {:?}", &self.config_path);
log::info!("reading config from {:?}", self.config_path);
let current_config = fs::read_to_string(&self.config_path)?;
let current_config = match current_config.parse::<DocumentMut>() {
@@ -548,7 +548,7 @@ impl Config {
}
pub fn write_back(&mut self) -> Result<(), io::Error> {
log::info!("writing config to {:?}", &self.config_path);
log::info!("writing config to {:?}", self.config_path);
/* the new config */
let new_config = self.config_toml.clone().unwrap_or_default();
let new_config = toml_edit::ser::to_string_pretty(&new_config).expect("config");

View File

@@ -1,9 +1,9 @@
use std::{collections::HashMap, net::IpAddr};
use std::{collections::HashMap, io, net::IpAddr};
use local_channel::mpsc::{Receiver, Sender, channel};
use tokio::net::lookup_host;
use tokio::task::{JoinHandle, spawn_local};
use hickory_resolver::{ResolveError, TokioResolver};
use tokio_util::sync::CancellationToken;
use lan_mouse_ipc::ClientHandle;
@@ -22,11 +22,10 @@ struct DnsRequest {
pub(crate) enum DnsEvent {
Resolving(ClientHandle),
Resolved(ClientHandle, String, Result<Vec<IpAddr>, ResolveError>),
Resolved(ClientHandle, String, io::Result<Vec<IpAddr>>),
}
struct DnsTask {
resolver: TokioResolver,
request_rx: Receiver<DnsRequest>,
event_tx: Sender<DnsEvent>,
cancellation_token: CancellationToken,
@@ -34,14 +33,12 @@ struct DnsTask {
}
impl DnsResolver {
pub(crate) fn new() -> Result<Self, ResolveError> {
let resolver = TokioResolver::builder_tokio()?.build();
pub(crate) fn new() -> io::Result<Self> {
let (request_tx, request_rx) = channel();
let (event_tx, event_rx) = channel();
let cancellation_token = CancellationToken::new();
let dns_task = DnsTask {
active_tasks: Default::default(),
resolver,
request_rx,
event_tx,
cancellation_token: cancellation_token.clone(),
@@ -97,15 +94,13 @@ impl DnsTask {
/* spawn task for dns request */
let event_tx = self.event_tx.clone();
let resolver = self.resolver.clone();
let cancellation_token = self.cancellation_token.clone();
let task = tokio::task::spawn_local(async move {
tokio::select! {
ips = resolver.lookup_ip(&hostname) => {
let ips = ips.map(|ips| ips.iter().collect::<Vec<_>>());
result = resolve_hostname(&hostname) => {
event_tx
.send(DnsEvent::Resolved(handle, hostname, ips))
.send(DnsEvent::Resolved(handle, hostname, result))
.expect("channel closed");
}
_ = cancellation_token.cancelled() => {},
@@ -115,3 +110,18 @@ impl DnsTask {
}
}
}
/// Resolve `hostname` via the operating system's full name-resolution
/// stack (`getaddrinfo` on Unix, GetAddrInfoEx on Windows). This walks
/// `/etc/nsswitch.conf` on Linux — picking up mDNS via Avahi, /etc/hosts,
/// and DNS — and uses Bonjour for `.local` names on macOS. Pure-DNS
/// resolvers like hickory miss all of those, which is why a Bonjour
/// hostname (e.g. `JKMBP-M4-Max.local`) wouldn't resolve before.
///
/// Port `0` is a placeholder — `lookup_host` requires `host:port` but we
/// only care about the IPs at this stage; the actual port is appended at
/// connection time.
async fn resolve_hostname(hostname: &str) -> io::Result<Vec<IpAddr>> {
let addrs = lookup_host((hostname, 0)).await?;
Ok(addrs.map(|sa| sa.ip()).collect())
}

View File

@@ -9,7 +9,6 @@ use crate::{
listen::{LanMouseListener, ListenerCreationError},
};
use futures::StreamExt;
use hickory_resolver::ResolveError;
use lan_mouse_ipc::{
AsyncFrontendListener, ClientHandle, FrontendEvent, FrontendRequest, IpcError,
IpcListenerCreationError, Position, Status,
@@ -26,8 +25,6 @@ use tokio::{process::Command, signal, sync::Notify};
#[derive(Debug, Error)]
pub enum ServiceError {
#[error(transparent)]
Dns(#[from] ResolveError),
#[error(transparent)]
IpcListen(#[from] IpcListenerCreationError),
#[error(transparent)]