Compare commits

..

2 Commits

Author SHA1 Message Date
Ferdinand Schober
33b6b78922 guard by feature flag 2026-05-15 15:16:36 +02:00
Ferdinand Schober
304d8a193f implement xdg-foreign to put capture dialog on top 2026-05-15 15:16:36 +02:00
39 changed files with 304 additions and 536 deletions

View File

@@ -174,16 +174,13 @@ jobs:
steps: steps:
- name: Download build artifacts - name: Download build artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
- name: Get short SHA
id: vars
run: echo "short_sha=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
- name: Create Pre-Release - name: Create Pre-Release
if: ${{ !startsWith(github.ref, 'refs/tags/') }} if: ${{ !startsWith(github.ref, 'refs/tags/') }}
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
tag_name: ${{ format('{0}-{1}', github.event.inputs.name || github.ref_name, steps.vars.outputs.short_sha) }} tag_name: ${{ github.event.inputs.name || github.ref_name }}
name: ${{ format('{0}-{1}', github.event.inputs.name || github.ref_name, steps.vars.outputs.short_sha) }} name: ${{ github.event.inputs.name || github.ref_name }}
prerelease: true prerelease: true
generate_release_notes: true generate_release_notes: true
files: | files: |

View File

@@ -28,7 +28,7 @@ Lan Mouse is an open-source Software KVM sharing mouse/keyboard input across loc
## Feature & cfg discipline ## Feature & cfg discipline
- Feature flags live in root `Cargo.toml`. Gate OS-specific modules with the configs exported in build.rs (e.g., `cfg(layer_shell)`). - Feature flags live in root `Cargo.toml`. Gate OS-specific modules with tight cfgs (e.g., `cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))`).
- Prefer module-level gating over per-function cfgs to avoid empty stubs. - Prefer module-level gating over per-function cfgs to avoid empty stubs.
- New backends: add feature in `Cargo.toml`, create gated module, log backend selection. - New backends: add feature in `Cargo.toml`, create gated module, log backend selection.

41
Cargo.lock generated
View File

@@ -1087,6 +1087,30 @@ dependencies = [
"system-deps", "system-deps",
] ]
[[package]]
name = "gdk4-wayland"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd34518488cd624a85e75e82540bc24c72cfeb0aea6bad7faed683ca3977dba0"
dependencies = [
"gdk4",
"gdk4-wayland-sys",
"gio",
"glib",
"libc",
]
[[package]]
name = "gdk4-wayland-sys"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c7a0f2332c531d62ee3f14f5e839ac1abac59e9b052adf1495124c00d89a34b"
dependencies = [
"glib-sys",
"libc",
"system-deps",
]
[[package]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.7" version = "0.14.7"
@@ -1647,7 +1671,7 @@ dependencies = [
[[package]] [[package]]
name = "input-capture" name = "input-capture"
version = "0.4.0" version = "0.3.0"
dependencies = [ dependencies = [
"ashpd", "ashpd",
"async-trait", "async-trait",
@@ -1677,7 +1701,7 @@ dependencies = [
[[package]] [[package]]
name = "input-emulation" name = "input-emulation"
version = "0.4.0" version = "0.3.0"
dependencies = [ dependencies = [
"ashpd", "ashpd",
"async-trait", "async-trait",
@@ -1703,7 +1727,7 @@ dependencies = [
[[package]] [[package]]
name = "input-event" name = "input-event"
version = "0.4.0" version = "0.3.0"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"log", "log",
@@ -1840,7 +1864,7 @@ dependencies = [
[[package]] [[package]]
name = "lan-mouse" name = "lan-mouse"
version = "0.11.0" version = "0.10.0"
dependencies = [ dependencies = [
"clap", "clap",
"env_logger", "env_logger",
@@ -1875,7 +1899,7 @@ dependencies = [
[[package]] [[package]]
name = "lan-mouse-cli" name = "lan-mouse-cli"
version = "0.3.0" version = "0.2.0"
dependencies = [ dependencies = [
"clap", "clap",
"futures", "futures",
@@ -1886,9 +1910,10 @@ dependencies = [
[[package]] [[package]]
name = "lan-mouse-gtk" name = "lan-mouse-gtk"
version = "0.3.0" version = "0.2.0"
dependencies = [ dependencies = [
"async-channel", "async-channel",
"gdk4-wayland",
"glib-build-tools", "glib-build-tools",
"gtk4", "gtk4",
"hostname", "hostname",
@@ -1900,7 +1925,7 @@ dependencies = [
[[package]] [[package]]
name = "lan-mouse-ipc" name = "lan-mouse-ipc"
version = "0.3.0" version = "0.2.0"
dependencies = [ dependencies = [
"futures", "futures",
"log", "log",
@@ -1913,7 +1938,7 @@ dependencies = [
[[package]] [[package]]
name = "lan-mouse-proto" name = "lan-mouse-proto"
version = "0.3.0" version = "0.2.0"
dependencies = [ dependencies = [
"input-event", "input-event",
"num_enum", "num_enum",

View File

@@ -12,7 +12,7 @@ members = [
[package] [package]
name = "lan-mouse" name = "lan-mouse"
description = "Software KVM Switch / mouse & keyboard sharing software for Local Area Networks" description = "Software KVM Switch / mouse & keyboard sharing software for Local Area Networks"
version = "0.11.0" version = "0.10.0"
edition = "2021" edition = "2021"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse" repository = "https://github.com/feschber/lan-mouse"
@@ -27,13 +27,13 @@ panic = "abort"
shadow-rs = "1.2.0" shadow-rs = "1.2.0"
[dependencies] [dependencies]
input-event = { path = "input-event", version = "0.4.0" } input-event = { path = "input-event", version = "0.3.0" }
input-emulation = { path = "input-emulation", version = "0.4.0", default-features = false } input-emulation = { path = "input-emulation", version = "0.3.0", default-features = false }
input-capture = { path = "input-capture", version = "0.4.0", default-features = false } input-capture = { path = "input-capture", version = "0.3.0", default-features = false }
lan-mouse-cli = { path = "lan-mouse-cli", version = "0.3.0" } lan-mouse-cli = { path = "lan-mouse-cli", version = "0.2.0" }
lan-mouse-gtk = { path = "lan-mouse-gtk", version = "0.3.0", optional = true } lan-mouse-gtk = { path = "lan-mouse-gtk", version = "0.2.0", optional = true }
lan-mouse-ipc = { path = "lan-mouse-ipc", version = "0.3.0" } lan-mouse-ipc = { path = "lan-mouse-ipc", version = "0.2.0" }
lan-mouse-proto = { path = "lan-mouse-proto", version = "0.3.0" } lan-mouse-proto = { path = "lan-mouse-proto", version = "0.2.0" }
shadow-rs = { version = "1.2.0", features = ["metadata"] } shadow-rs = { version = "1.2.0", features = ["metadata"] }
hickory-resolver = "0.25.2" hickory-resolver = "0.25.2"

View File

@@ -5,57 +5,4 @@ fn main() {
.deny_const(Default::default()) .deny_const(Default::default())
.build() .build()
.expect("shadow build"); .expect("shadow build");
let unix = cfg!(unix);
let macos = cfg!(target_os = "macos");
let layer_shell_capture = cfg!(feature = "layer_shell_capture");
let libei_capture = cfg!(feature = "libei_capture");
let x11_capture = cfg!(feature = "x11_capture");
let libei_emulation = cfg!(feature = "libei_emulation");
let x11_emulation = cfg!(feature = "x11_emulation");
let wlroots_emulation = cfg!(feature = "wlroots_emulation");
let rdp_emulation = cfg!(feature = "rdp_emulation");
let layer_shell_capture = unix && !macos && layer_shell_capture;
let libei_capture = unix && !macos && libei_capture;
let x11_capture = unix && !macos && x11_capture;
let libei_emulation = unix && !macos && libei_emulation;
let rdp_emulation = unix && !macos && rdp_emulation;
let wlroots_emulation = unix && !macos && wlroots_emulation;
let x11_emulation = unix && !macos && x11_emulation;
println!("cargo::rustc-check-cfg=cfg(layer_shell_capture)");
println!("cargo::rustc-check-cfg=cfg(libei_capture)");
println!("cargo::rustc-check-cfg=cfg(x11_capture)");
println!("cargo::rustc-check-cfg=cfg(libei_emulation)");
println!("cargo::rustc-check-cfg=cfg(rdp_emulation)");
println!("cargo::rustc-check-cfg=cfg(wlroots_emulation)");
println!("cargo::rustc-check-cfg=cfg(x11_emulation)");
if layer_shell_capture {
println!("cargo::rustc-cfg=layer_shell_capture");
}
if libei_capture {
println!("cargo::rustc-cfg=libei_capture");
}
if x11_capture {
println!("cargo::rustc-cfg=x11_capture");
}
if libei_emulation {
println!("cargo::rustc-cfg=libei_emulation");
}
if rdp_emulation {
println!("cargo::rustc-cfg=rdp_emulation");
}
if wlroots_emulation {
println!("cargo::rustc-cfg=wlroots_emulation");
}
if x11_emulation {
println!("cargo::rustc-cfg=x11_emulation");
}
} }

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "input-capture" name = "input-capture"
description = "cross-platform input-capture library used by lan-mouse" description = "cross-platform input-capture library used by lan-mouse"
version = "0.4.0" version = "0.3.0"
edition = "2021" edition = "2021"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse" repository = "https://github.com/feschber/lan-mouse"
@@ -10,7 +10,7 @@ repository = "https://github.com/feschber/lan-mouse"
futures = "0.3.28" futures = "0.3.28"
futures-core = "0.3.30" futures-core = "0.3.30"
log = "0.4.22" log = "0.4.22"
input-event = { path = "../input-event", version = "0.4.0" } input-event = { path = "../input-event", version = "0.3.0" }
memmap = "0.7" memmap = "0.7"
tempfile = "3.25.0" tempfile = "3.25.0"
thiserror = "2.0.0" thiserror = "2.0.0"

View File

@@ -1,25 +0,0 @@
fn main() {
let unix = cfg!(unix);
let layer_shell = cfg!(feature = "layer_shell");
let libei = cfg!(feature = "libei");
let x11 = cfg!(feature = "x11");
let macos = cfg!(target_os = "macos");
let libei = unix && !macos && libei;
let layer_shell = unix && !macos && layer_shell;
let x11 = unix && !macos && x11;
println!("cargo::rustc-check-cfg=cfg(layer_shell)");
println!("cargo::rustc-check-cfg=cfg(libei)");
println!("cargo::rustc-check-cfg=cfg(x11)");
if layer_shell {
println!("cargo::rustc-cfg=layer_shell");
}
if libei {
println!("cargo::rustc-cfg=libei");
}
if x11 {
println!("cargo::rustc-cfg=x11");
}
}

View File

@@ -8,16 +8,16 @@ pub enum InputCaptureError {
Capture(#[from] CaptureError), Capture(#[from] CaptureError),
} }
#[cfg(layer_shell)] #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
use std::io; use std::io;
#[cfg(layer_shell)] #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
use wayland_client::{ use wayland_client::{
ConnectError, DispatchError, ConnectError, DispatchError,
backend::WaylandError, backend::WaylandError,
globals::{BindError, GlobalError}, globals::{BindError, GlobalError},
}; };
#[cfg(libei)] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
use ashpd::desktop::ResponseError; use ashpd::desktop::ResponseError;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@@ -31,13 +31,13 @@ pub enum CaptureError {
EndOfStream, EndOfStream,
#[error("io error: `{0}`")] #[error("io error: `{0}`")]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[cfg(libei)] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("libei error: `{0}`")] #[error("libei error: `{0}`")]
Reis(#[from] reis::Error), Reis(#[from] reis::Error),
#[cfg(libei)] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error(transparent)] #[error(transparent)]
Portal(#[from] ashpd::Error), Portal(#[from] ashpd::Error),
#[cfg(libei)] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("libei disconnected - reason: `{0}`")] #[error("libei disconnected - reason: `{0}`")]
Disconnected(String), Disconnected(String),
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@@ -61,13 +61,13 @@ pub enum CaptureError {
pub enum CaptureCreationError { pub enum CaptureCreationError {
#[error("no backend available")] #[error("no backend available")]
NoAvailableBackend, NoAvailableBackend,
#[cfg(libei)] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("error creating input-capture-portal backend: `{0}`")] #[error("error creating input-capture-portal backend: `{0}`")]
Libei(#[from] LibeiCaptureCreationError), Libei(#[from] LibeiCaptureCreationError),
#[cfg(layer_shell)] #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
#[error("error creating layer-shell capture backend: `{0}`")] #[error("error creating layer-shell capture backend: `{0}`")]
LayerShell(#[from] LayerShellCaptureCreationError), LayerShell(#[from] LayerShellCaptureCreationError),
#[cfg(x11)] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
#[error("error creating x11 capture backend: `{0}`")] #[error("error creating x11 capture backend: `{0}`")]
X11(#[from] X11InputCaptureCreationError), X11(#[from] X11InputCaptureCreationError),
#[cfg(windows)] #[cfg(windows)]
@@ -80,7 +80,7 @@ pub enum CaptureCreationError {
impl CaptureCreationError { impl CaptureCreationError {
/// request was intentionally denied by the user /// request was intentionally denied by the user
#[cfg(libei)] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
pub(crate) fn cancelled_by_user(&self) -> bool { pub(crate) fn cancelled_by_user(&self) -> bool {
matches!( matches!(
self, self,
@@ -89,20 +89,20 @@ impl CaptureCreationError {
))) )))
) )
} }
#[cfg(not(libei))] #[cfg(not(all(unix, feature = "libei", not(target_os = "macos"))))]
pub(crate) fn cancelled_by_user(&self) -> bool { pub(crate) fn cancelled_by_user(&self) -> bool {
false false
} }
} }
#[cfg(libei)] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum LibeiCaptureCreationError { pub enum LibeiCaptureCreationError {
#[error("xdg-desktop-portal: `{0}`")] #[error("xdg-desktop-portal: `{0}`")]
Ashpd(#[from] ashpd::Error), Ashpd(#[from] ashpd::Error),
} }
#[cfg(layer_shell)] #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
#[derive(Debug, Error)] #[derive(Debug, Error)]
#[error("{protocol} protocol not supported: {inner}")] #[error("{protocol} protocol not supported: {inner}")]
pub struct WaylandBindError { pub struct WaylandBindError {
@@ -110,14 +110,14 @@ pub struct WaylandBindError {
protocol: &'static str, protocol: &'static str,
} }
#[cfg(layer_shell)] #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
impl WaylandBindError { impl WaylandBindError {
pub(crate) fn new(inner: BindError, protocol: &'static str) -> Self { pub(crate) fn new(inner: BindError, protocol: &'static str) -> Self {
Self { inner, protocol } Self { inner, protocol }
} }
} }
#[cfg(layer_shell)] #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum LayerShellCaptureCreationError { pub enum LayerShellCaptureCreationError {
#[error(transparent)] #[error(transparent)]
@@ -134,7 +134,7 @@ pub enum LayerShellCaptureCreationError {
Io(#[from] io::Error), Io(#[from] io::Error),
} }
#[cfg(x11)] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum X11InputCaptureCreationError { pub enum X11InputCaptureCreationError {
#[error("X11 input capture is not yet implemented :(")] #[error("X11 input capture is not yet implemented :(")]

View File

@@ -2,6 +2,7 @@ use std::{
collections::{HashMap, HashSet, VecDeque}, collections::{HashMap, HashSet, VecDeque},
fmt::Display, fmt::Display,
mem::swap, mem::swap,
sync::{Arc, Mutex},
task::{Poll, ready}, task::{Poll, ready},
}; };
@@ -15,19 +16,19 @@ pub use error::{CaptureCreationError, CaptureError, InputCaptureError};
pub mod error; pub mod error;
#[cfg(libei)] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
mod libei; mod libei;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
mod macos; mod macos;
#[cfg(layer_shell)] #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
mod layer_shell; mod layer_shell;
#[cfg(windows)] #[cfg(windows)]
mod windows; mod windows;
#[cfg(x11)] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
mod x11; mod x11;
/// fallback input capture (does not produce events) /// fallback input capture (does not produce events)
@@ -85,11 +86,11 @@ impl Display for Position {
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Backend { pub enum Backend {
#[cfg(libei)] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
InputCapturePortal, InputCapturePortal,
#[cfg(layer_shell)] #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
LayerShell, LayerShell,
#[cfg(x11)] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
X11, X11,
#[cfg(windows)] #[cfg(windows)]
Windows, Windows,
@@ -101,11 +102,11 @@ pub enum Backend {
impl Display for Backend { impl Display for Backend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
#[cfg(libei)] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::InputCapturePortal => write!(f, "input-capture-portal"), Backend::InputCapturePortal => write!(f, "input-capture-portal"),
#[cfg(layer_shell)] #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
Backend::LayerShell => write!(f, "layer-shell"), Backend::LayerShell => write!(f, "layer-shell"),
#[cfg(x11)] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11 => write!(f, "X11"), Backend::X11 => write!(f, "X11"),
#[cfg(windows)] #[cfg(windows)]
Backend::Windows => write!(f, "windows"), Backend::Windows => write!(f, "windows"),
@@ -129,6 +130,24 @@ pub struct InputCapture {
pending: VecDeque<(CaptureHandle, CaptureEvent)>, pending: VecDeque<(CaptureHandle, CaptureEvent)>,
} }
#[derive(Clone, Debug)]
pub enum WindowIdentifier {
Wayland(String),
X11(u32),
}
#[cfg(all(unix, feature = "libei"))]
impl Into<ashpd::WindowIdentifier> for WindowIdentifier {
fn into(self) -> ashpd::WindowIdentifier {
match self {
WindowIdentifier::Wayland(handle) => {
ashpd::WindowIdentifier::from_xdg_foreign_exported(handle)
}
WindowIdentifier::X11(_) => todo!(),
}
}
}
impl InputCapture { impl InputCapture {
/// create a new client with the given id /// create a new client with the given id
pub async fn create(&mut self, id: CaptureHandle, pos: Position) -> Result<(), CaptureError> { pub async fn create(&mut self, id: CaptureHandle, pos: Position) -> Result<(), CaptureError> {
@@ -190,8 +209,11 @@ impl InputCapture {
} }
/// creates a new [`InputCapture`] /// creates a new [`InputCapture`]
pub async fn new(backend: Option<Backend>) -> Result<Self, CaptureCreationError> { pub async fn new(
let capture = create(backend).await?; backend: Option<Backend>,
window_identifier: Arc<Mutex<Option<WindowIdentifier>>>,
) -> Result<Self, CaptureCreationError> {
let capture = create(backend, window_identifier).await?;
Ok(Self { Ok(Self {
capture, capture,
id_map: Default::default(), id_map: Default::default(),
@@ -293,16 +315,19 @@ trait Capture: Stream<Item = Result<(Position, CaptureEvent), CaptureError>> + U
async fn create_backend( async fn create_backend(
backend: Backend, backend: Backend,
window_identifier: Arc<Mutex<Option<WindowIdentifier>>>,
) -> Result< ) -> Result<
Box<dyn Capture<Item = Result<(Position, CaptureEvent), CaptureError>>>, Box<dyn Capture<Item = Result<(Position, CaptureEvent), CaptureError>>>,
CaptureCreationError, CaptureCreationError,
> { > {
match backend { match backend {
#[cfg(libei)] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::InputCapturePortal => Ok(Box::new(libei::LibeiInputCapture::new().await?)), Backend::InputCapturePortal => Ok(Box::new(
#[cfg(layer_shell)] libei::LibeiInputCapture::new(window_identifier).await?,
)),
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
Backend::LayerShell => Ok(Box::new(layer_shell::LayerShellInputCapture::new()?)), Backend::LayerShell => Ok(Box::new(layer_shell::LayerShellInputCapture::new()?)),
#[cfg(x11)] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11 => Ok(Box::new(x11::X11InputCapture::new()?)), Backend::X11 => Ok(Box::new(x11::X11InputCapture::new()?)),
#[cfg(windows)] #[cfg(windows)]
Backend::Windows => Ok(Box::new(windows::WindowsInputCapture::new())), Backend::Windows => Ok(Box::new(windows::WindowsInputCapture::new())),
@@ -314,12 +339,13 @@ async fn create_backend(
async fn create( async fn create(
backend: Option<Backend>, backend: Option<Backend>,
window_identifier: Arc<Mutex<Option<WindowIdentifier>>>,
) -> Result< ) -> Result<
Box<dyn Capture<Item = Result<(Position, CaptureEvent), CaptureError>>>, Box<dyn Capture<Item = Result<(Position, CaptureEvent), CaptureError>>>,
CaptureCreationError, CaptureCreationError,
> { > {
if let Some(backend) = backend { if let Some(backend) = backend {
let b = create_backend(backend).await; let b = create_backend(backend, window_identifier).await;
if b.is_ok() { if b.is_ok() {
log::info!("using capture backend: {backend}"); log::info!("using capture backend: {backend}");
} }
@@ -327,18 +353,18 @@ async fn create(
} }
for backend in [ for backend in [
#[cfg(libei)] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::InputCapturePortal, Backend::InputCapturePortal,
#[cfg(layer_shell)] #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
Backend::LayerShell, Backend::LayerShell,
#[cfg(x11)] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11, Backend::X11,
#[cfg(windows)] #[cfg(windows)]
Backend::Windows, Backend::Windows,
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
Backend::MacOs, Backend::MacOs,
] { ] {
match create_backend(backend).await { match create_backend(backend, window_identifier.clone()).await {
Ok(b) => { Ok(b) => {
log::info!("using capture backend: {backend}"); log::info!("using capture backend: {backend}");
return Ok(b); return Ok(b);

View File

@@ -23,7 +23,7 @@ use std::{
os::unix::net::UnixStream, os::unix::net::UnixStream,
pin::Pin, pin::Pin,
rc::Rc, rc::Rc,
sync::Arc, sync::{Arc, Mutex},
task::{Context, Poll}, task::{Context, Poll},
}; };
use tokio::{ use tokio::{
@@ -39,7 +39,7 @@ use futures_core::Stream;
use input_event::Event; use input_event::Event;
use crate::CaptureEvent; use crate::{CaptureEvent, WindowIdentifier};
use super::{ use super::{
Capture as LanMouseInputCapture, Position, Capture as LanMouseInputCapture, Position,
@@ -161,13 +161,17 @@ async fn update_barriers(
async fn create_session( async fn create_session(
input_capture: &InputCapture, input_capture: &InputCapture,
window_identifier: Arc<Mutex<Option<WindowIdentifier>>>,
) -> std::result::Result<(Session<InputCapture>, BitFlags<Capabilities>), ashpd::Error> { ) -> std::result::Result<(Session<InputCapture>, BitFlags<Capabilities>), ashpd::Error> {
log::debug!("creating input capture session"); log::debug!("creating input capture session: {window_identifier:?}");
let window_identifier = window_identifier.lock().unwrap().clone();
let create_session_options = CreateSessionOptions::default().set_capabilities( let create_session_options = CreateSessionOptions::default().set_capabilities(
Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen, Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen,
); );
let ashpd_window_identifier: Option<ashpd::WindowIdentifier> =
window_identifier.map(|i| i.into());
input_capture input_capture
.create_session(None, create_session_options) .create_session(ashpd_window_identifier.as_ref(), create_session_options)
.await .await
} }
@@ -212,10 +216,15 @@ async fn libei_event_handler(
} }
impl LibeiInputCapture { impl LibeiInputCapture {
pub async fn new() -> std::result::Result<Self, LibeiCaptureCreationError> { /// creates a new libei input capture
/// `window_id` is a window identifier for user prompts
pub async fn new(
window_identifier: Arc<Mutex<Option<WindowIdentifier>>>,
) -> std::result::Result<Self, LibeiCaptureCreationError> {
let input_capture = Box::pin(InputCapture::new().await?); let input_capture = Box::pin(InputCapture::new().await?);
let input_capture_ptr = input_capture.as_ref().get_ref() as *const InputCapture; let input_capture_ptr = input_capture.as_ref().get_ref() as *const InputCapture;
let first_session = Some(create_session(unsafe { &*input_capture_ptr }).await?); let first_session =
Some(create_session(unsafe { &*input_capture_ptr }, window_identifier.clone()).await?);
let (event_tx, event_rx) = mpsc::channel(1); let (event_tx, event_rx) = mpsc::channel(1);
let (notify_capture, notify_rx) = mpsc::channel(1); let (notify_capture, notify_rx) = mpsc::channel(1);
@@ -230,6 +239,7 @@ impl LibeiInputCapture {
first_session, first_session,
event_tx, event_tx,
cancellation_token.clone(), cancellation_token.clone(),
window_identifier,
); );
let capture_task = tokio::task::spawn_local(capture); let capture_task = tokio::task::spawn_local(capture);
@@ -254,6 +264,7 @@ async fn do_capture(
session: Option<(Session<InputCapture>, BitFlags<Capabilities>)>, session: Option<(Session<InputCapture>, BitFlags<Capabilities>)>,
event_tx: Sender<(Position, CaptureEvent)>, event_tx: Sender<(Position, CaptureEvent)>,
cancellation_token: CancellationToken, cancellation_token: CancellationToken,
window_identifier: Arc<Mutex<Option<WindowIdentifier>>>,
) -> Result<(), CaptureError> { ) -> Result<(), CaptureError> {
let mut session = session.map(|s| s.0); let mut session = session.map(|s| s.0);
@@ -299,7 +310,11 @@ async fn do_capture(
// create session // create session
let mut session = match session.take() { let mut session = match session.take() {
Some(s) => s, Some(s) => s,
None => create_session(input_capture).await?.0, None => {
create_session(input_capture, window_identifier.clone())
.await?
.0
}
}; };
let capture_session = do_capture_session( let capture_session = do_capture_session(

View File

@@ -222,8 +222,11 @@ fn start_routine(
} }
/* run message loop */ /* run message loop */
while let Some(msg) = get_msg() { loop {
// mouse / keybrd proc do not actually return a message // mouse / keybrd proc do not actually return a message
let Some(msg) = get_msg() else {
break;
};
if msg.hwnd.0.is_null() { if msg.hwnd.0.is_null() {
/* messages sent via PostThreadMessage */ /* messages sent via PostThreadMessage */
match msg.wParam.0 { match msg.wParam.0 {

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "input-emulation" name = "input-emulation"
description = "cross-platform input emulation library used by lan-mouse" description = "cross-platform input emulation library used by lan-mouse"
version = "0.4.0" version = "0.3.0"
edition = "2021" edition = "2021"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse" repository = "https://github.com/feschber/lan-mouse"
@@ -10,7 +10,7 @@ repository = "https://github.com/feschber/lan-mouse"
async-trait = "0.1.80" async-trait = "0.1.80"
futures = "0.3.28" futures = "0.3.28"
log = "0.4.22" log = "0.4.22"
input-event = { path = "../input-event", version = "0.4.0" } input-event = { path = "../input-event", version = "0.3.0" }
thiserror = "2.0.0" thiserror = "2.0.0"
tokio = { version = "1.32.0", features = [ tokio = { version = "1.32.0", features = [
"io-util", "io-util",

View File

@@ -1,31 +0,0 @@
fn main() {
let unix = cfg!(unix);
let libei = cfg!(feature = "libei");
let x11 = cfg!(feature = "x11");
let macos = cfg!(target_os = "macos");
let wlroots = cfg!(feature = "wlroots");
let rdp = cfg!(feature = "remote_desktop_portal");
let libei = unix && !macos && libei;
let wlroots = unix && !macos && wlroots;
let x11 = unix && !macos && x11;
let rdp = unix && !macos && rdp;
println!("cargo::rustc-check-cfg=cfg(wlroots)");
println!("cargo::rustc-check-cfg=cfg(libei)");
println!("cargo::rustc-check-cfg=cfg(x11)");
println!("cargo::rustc-check-cfg=cfg(rdp)");
if libei {
println!("cargo::rustc-cfg=libei");
}
if x11 {
println!("cargo::rustc-cfg=x11");
}
if wlroots {
println!("cargo::rustc-cfg=wlroots");
}
if rdp {
println!("cargo::rustc-cfg=rdp");
}
}

View File

@@ -6,12 +6,16 @@ pub enum InputEmulationError {
Emulate(#[from] EmulationError), Emulate(#[from] EmulationError),
} }
#[cfg(any(libei, rdp))] #[cfg(all(
unix,
any(feature = "remote_desktop_portal", feature = "libei"),
not(target_os = "macos")
))]
use ashpd::{Error::Response, desktop::ResponseError}; use ashpd::{Error::Response, desktop::ResponseError};
use std::io; use std::io;
use thiserror::Error; use thiserror::Error;
#[cfg(wlroots)] #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
use wayland_client::{ use wayland_client::{
ConnectError, DispatchError, ConnectError, DispatchError,
backend::WaylandError, backend::WaylandError,
@@ -22,13 +26,17 @@ use wayland_client::{
pub enum EmulationError { pub enum EmulationError {
#[error("event stream closed")] #[error("event stream closed")]
EndOfStream, EndOfStream,
#[cfg(libei)] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("libei error: `{0}`")] #[error("libei error: `{0}`")]
Libei(#[from] reis::Error), Libei(#[from] reis::Error),
#[cfg(wlroots)] #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
#[error("wayland error: `{0}`")] #[error("wayland error: `{0}`")]
Wayland(#[from] wayland_client::backend::WaylandError), Wayland(#[from] wayland_client::backend::WaylandError),
#[cfg(any(rdp, libei))] #[cfg(all(
unix,
any(feature = "remote_desktop_portal", feature = "libei"),
not(target_os = "macos")
))]
#[error("xdg-desktop-portal: `{0}`")] #[error("xdg-desktop-portal: `{0}`")]
Ashpd(#[from] ashpd::Error), Ashpd(#[from] ashpd::Error),
#[error("io error: `{0}`")] #[error("io error: `{0}`")]
@@ -37,16 +45,16 @@ pub enum EmulationError {
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum EmulationCreationError { pub enum EmulationCreationError {
#[cfg(wlroots)] #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
#[error("wlroots backend: `{0}`")] #[error("wlroots backend: `{0}`")]
Wlroots(#[from] WlrootsEmulationCreationError), Wlroots(#[from] WlrootsEmulationCreationError),
#[cfg(libei)] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("libei backend: `{0}`")] #[error("libei backend: `{0}`")]
Libei(#[from] LibeiEmulationCreationError), Libei(#[from] LibeiEmulationCreationError),
#[cfg(rdp)] #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
#[error("xdg-desktop-portal: `{0}`")] #[error("xdg-desktop-portal: `{0}`")]
Xdp(#[from] XdpEmulationCreationError), Xdp(#[from] XdpEmulationCreationError),
#[cfg(x11)] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
#[error("x11: `{0}`")] #[error("x11: `{0}`")]
X11(#[from] X11EmulationCreationError), X11(#[from] X11EmulationCreationError),
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@@ -62,7 +70,7 @@ pub enum EmulationCreationError {
impl EmulationCreationError { impl EmulationCreationError {
/// request was intentionally denied by the user /// request was intentionally denied by the user
pub(crate) fn cancelled_by_user(&self) -> bool { pub(crate) fn cancelled_by_user(&self) -> bool {
#[cfg(libei)] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
if matches!( if matches!(
self, self,
EmulationCreationError::Libei(LibeiEmulationCreationError::Ashpd(Response( EmulationCreationError::Libei(LibeiEmulationCreationError::Ashpd(Response(
@@ -71,7 +79,7 @@ impl EmulationCreationError {
) { ) {
return true; return true;
} }
#[cfg(rdp)] #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
if matches!( if matches!(
self, self,
EmulationCreationError::Xdp(XdpEmulationCreationError::Ashpd(Response( EmulationCreationError::Xdp(XdpEmulationCreationError::Ashpd(Response(
@@ -84,7 +92,7 @@ impl EmulationCreationError {
} }
} }
#[cfg(wlroots)] #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum WlrootsEmulationCreationError { pub enum WlrootsEmulationCreationError {
#[error(transparent)] #[error(transparent)]
@@ -101,7 +109,7 @@ pub enum WlrootsEmulationCreationError {
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
} }
#[cfg(wlroots)] #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
#[derive(Debug, Error)] #[derive(Debug, Error)]
#[error("wayland protocol \"{protocol}\" not supported: {inner}")] #[error("wayland protocol \"{protocol}\" not supported: {inner}")]
pub struct WaylandBindError { pub struct WaylandBindError {
@@ -109,14 +117,14 @@ pub struct WaylandBindError {
protocol: &'static str, protocol: &'static str,
} }
#[cfg(wlroots)] #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
impl WaylandBindError { impl WaylandBindError {
pub(crate) fn new(inner: BindError, protocol: &'static str) -> Self { pub(crate) fn new(inner: BindError, protocol: &'static str) -> Self {
Self { inner, protocol } Self { inner, protocol }
} }
} }
#[cfg(libei)] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum LibeiEmulationCreationError { pub enum LibeiEmulationCreationError {
#[error(transparent)] #[error(transparent)]
@@ -127,14 +135,14 @@ pub enum LibeiEmulationCreationError {
Reis(#[from] reis::Error), Reis(#[from] reis::Error),
} }
#[cfg(rdp)] #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum XdpEmulationCreationError { pub enum XdpEmulationCreationError {
#[error(transparent)] #[error(transparent)]
Ashpd(#[from] ashpd::Error), Ashpd(#[from] ashpd::Error),
} }
#[cfg(x11)] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum X11EmulationCreationError { pub enum X11EmulationCreationError {
#[error("could not open display")] #[error("could not open display")]

View File

@@ -11,16 +11,16 @@ pub use self::error::{EmulationCreationError, EmulationError, InputEmulationErro
#[cfg(windows)] #[cfg(windows)]
mod windows; mod windows;
#[cfg(x11)] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
mod x11; mod x11;
#[cfg(wlroots)] #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
mod wlroots; mod wlroots;
#[cfg(rdp)] #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
mod xdg_desktop_portal; mod xdg_desktop_portal;
#[cfg(libei)] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
mod libei; mod libei;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@@ -34,13 +34,13 @@ pub type EmulationHandle = u64;
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Backend { pub enum Backend {
#[cfg(wlroots)] #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
Wlroots, Wlroots,
#[cfg(libei)] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Libei, Libei,
#[cfg(rdp)] #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
Xdp, Xdp,
#[cfg(x11)] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
X11, X11,
#[cfg(windows)] #[cfg(windows)]
Windows, Windows,
@@ -52,13 +52,13 @@ pub enum Backend {
impl Display for Backend { impl Display for Backend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
#[cfg(wlroots)] #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
Backend::Wlroots => write!(f, "wlroots"), Backend::Wlroots => write!(f, "wlroots"),
#[cfg(libei)] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::Libei => write!(f, "libei"), Backend::Libei => write!(f, "libei"),
#[cfg(rdp)] #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
Backend::Xdp => write!(f, "xdg-desktop-portal"), Backend::Xdp => write!(f, "xdg-desktop-portal"),
#[cfg(x11)] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11 => write!(f, "X11"), Backend::X11 => write!(f, "X11"),
#[cfg(windows)] #[cfg(windows)]
Backend::Windows => write!(f, "windows"), Backend::Windows => write!(f, "windows"),
@@ -78,13 +78,13 @@ pub struct InputEmulation {
impl InputEmulation { impl InputEmulation {
async fn with_backend(backend: Backend) -> Result<InputEmulation, EmulationCreationError> { async fn with_backend(backend: Backend) -> Result<InputEmulation, EmulationCreationError> {
let emulation: Box<dyn Emulation> = match backend { let emulation: Box<dyn Emulation> = match backend {
#[cfg(wlroots)] #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
Backend::Wlroots => Box::new(wlroots::WlrootsEmulation::new()?), Backend::Wlroots => Box::new(wlroots::WlrootsEmulation::new()?),
#[cfg(libei)] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::Libei => Box::new(libei::LibeiEmulation::new().await?), Backend::Libei => Box::new(libei::LibeiEmulation::new().await?),
#[cfg(x11)] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11 => Box::new(x11::X11Emulation::new()?), Backend::X11 => Box::new(x11::X11Emulation::new()?),
#[cfg(rdp)] #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
Backend::Xdp => Box::new(xdg_desktop_portal::DesktopPortalEmulation::new().await?), Backend::Xdp => Box::new(xdg_desktop_portal::DesktopPortalEmulation::new().await?),
#[cfg(windows)] #[cfg(windows)]
Backend::Windows => Box::new(windows::WindowsEmulation::new()?), Backend::Windows => Box::new(windows::WindowsEmulation::new()?),
@@ -109,13 +109,13 @@ impl InputEmulation {
} }
for backend in [ for backend in [
#[cfg(wlroots)] #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
Backend::Wlroots, Backend::Wlroots,
#[cfg(libei)] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::Libei, Backend::Libei,
#[cfg(rdp)] #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
Backend::Xdp, Backend::Xdp,
#[cfg(x11)] #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11, Backend::X11,
#[cfg(windows)] #[cfg(windows)]
Backend::Windows, Backend::Windows,

View File

@@ -106,19 +106,8 @@ impl MacOSEmulation {
} }
} }
} }
// Always release the key with the correct CGKeyCode, regardless of // release key when cancelled
// whether the repeat loop ran. This matches @feschber's review update_modifiers(&modifiers, key as u32, 0);
// request: "still release the key repeat task but with the correct
// code."
//
// Do NOT call update_modifiers here: `key` is a Mac CGKeyCode but
// update_modifiers expects a Linux evdev scancode, and the two
// codespaces collide (e.g. Mac LeftShift=56 == Linux KeyLeftAlt=56,
// Mac Down=125 == Linux KeyLeftMeta=125), corrupting modifier
// state for chords like Shift+Option+X or Cmd+Down. Modifier state
// is owned by the main consume() loop, which already calls
// update_modifiers with the correct Linux scancode on the real key
// release event from the client.
key_event(event_source.clone(), key, 0, modifiers.get()); key_event(event_source.clone(), key, 0, modifiers.get());
}); });
self.repeat_task = Some(repeat_task); self.repeat_task = Some(repeat_task);
@@ -168,19 +157,6 @@ extern "C" {
fn AXIsProcessTrusted() -> bool; fn AXIsProcessTrusted() -> bool;
} }
/// Mac virtual key codes for the four arrow keys.
const MAC_KEY_LEFT: u16 = 0x7B;
const MAC_KEY_RIGHT: u16 = 0x7C;
const MAC_KEY_DOWN: u16 = 0x7D;
const MAC_KEY_UP: u16 = 0x7E;
fn is_arrow_key(key: u16) -> bool {
matches!(
key,
MAC_KEY_LEFT | MAC_KEY_RIGHT | MAC_KEY_DOWN | MAC_KEY_UP
)
}
fn key_event(event_source: CGEventSource, key: u16, state: u8, modifiers: XMods) { fn key_event(event_source: CGEventSource, key: u16, state: u8, modifiers: XMods) {
let event = match CGEvent::new_keyboard_event(event_source, key, state != 0) { let event = match CGEvent::new_keyboard_event(event_source, key, state != 0) {
Ok(e) => e, Ok(e) => e,
@@ -189,15 +165,7 @@ fn key_event(event_source: CGEventSource, key: u16, state: u8, modifiers: XMods)
return; return;
} }
}; };
let mut flags = to_cgevent_flags(modifiers); event.set_flags(to_cgevent_flags(modifiers));
// Hardware-generated arrow keys on macOS carry NumericPad + SecondaryFn.
// CGEventTap-based hotkey matchers (e.g. tiling window managers) check
// these flags to recognize navigation keys; without them synthesized
// arrow chords fall through to the focused app.
if is_arrow_key(key) {
flags |= CGEventFlags::CGEventFlagNumericPad | CGEventFlags::CGEventFlagSecondaryFn;
}
event.set_flags(flags);
event.post(CGEventTapLocation::HID); event.post(CGEventTapLocation::HID);
log::trace!("key event: {key} {state}"); log::trace!("key event: {key} {state}");
} }

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "input-event" name = "input-event"
description = "cross-platform input-event types for input-capture / input-emulation" description = "cross-platform input-event types for input-capture / input-emulation"
version = "0.4.0" version = "0.3.0"
edition = "2021" edition = "2021"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse" repository = "https://github.com/feschber/lan-mouse"

View File

@@ -1,14 +1,14 @@
[package] [package]
name = "lan-mouse-cli" name = "lan-mouse-cli"
description = "CLI Frontend for lan-mouse" description = "CLI Frontend for lan-mouse"
version = "0.3.0" version = "0.2.0"
edition = "2021" edition = "2021"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse" repository = "https://github.com/feschber/lan-mouse"
[dependencies] [dependencies]
futures = "0.3.30" futures = "0.3.30"
lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.3.0" } lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.2.0" }
clap = { version = "4.4.11", features = ["derive"] } clap = { version = "4.4.11", features = ["derive"] }
thiserror = "2.0.0" thiserror = "2.0.0"
tokio = { version = "1.32.0", features = [ tokio = { version = "1.32.0", features = [

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "lan-mouse-gtk" name = "lan-mouse-gtk"
description = "GTK4 / Libadwaita Frontend for lan-mouse" description = "GTK4 / Libadwaita Frontend for lan-mouse"
version = "0.3.0" version = "0.2.0"
edition = "2021" edition = "2021"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse" repository = "https://github.com/feschber/lan-mouse"
@@ -12,8 +12,16 @@ adw = { package = "libadwaita", version = "0.7.0", features = ["v1_1"] }
async-channel = { version = "2.1.1" } async-channel = { version = "2.1.1" }
hostname = "0.4.0" hostname = "0.4.0"
log = "0.4.20" log = "0.4.20"
lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.3.0" } lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.2.0" }
thiserror = "2.0.0" thiserror = "2.0.0"
[target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
gdk4_wayland = { package = "gdk4-wayland", version="0.9.6", optional=true }
[build-dependencies] [build-dependencies]
glib-build-tools = { version = "0.20.0" } glib-build-tools = { version = "0.20.0" }
[features]
default = ["wayland_window_identifier"]
wayland_window_identifier = ["dep:gdk4_wayland"]

View File

@@ -6,7 +6,6 @@
<file compressed="true" preprocess="xml-stripblanks">fingerprint_window.ui</file> <file compressed="true" preprocess="xml-stripblanks">fingerprint_window.ui</file>
<file compressed="true" preprocess="xml-stripblanks">client_row.ui</file> <file compressed="true" preprocess="xml-stripblanks">client_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">key_row.ui</file> <file compressed="true" preprocess="xml-stripblanks">key_row.ui</file>
<file compressed="true">style.css</file>
</gresource> </gresource>
<gresource prefix="/de/feschber/LanMouse/icons"> <gresource prefix="/de/feschber/LanMouse/icons">
<file compressed="true" preprocess="xml-stripblanks">de.feschber.LanMouse.svg</file> <file compressed="true" preprocess="xml-stripblanks">de.feschber.LanMouse.svg</file>

View File

@@ -1,12 +0,0 @@
.peer-match > box > list .subtitle {
color: @success_color;
}
.peer-mismatch > box > list .subtitle {
font-weight: bold;
color: @warning_color;
}
.peer-unknown > box > list .subtitle {
color: @warning_color;
}

View File

@@ -26,7 +26,6 @@ impl ClientObject {
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
) )
.property("resolving", state.resolving) .property("resolving", state.resolving)
.property("peer-commit", peer_commit_to_string(state.peer_commit))
.build() .build()
} }
@@ -35,14 +34,6 @@ impl ClientObject {
} }
} }
/// Render the 8-byte ASCII commit hash carried in
/// [`lan_mouse_ipc::ClientState::peer_commit`] as a `String`. `None`
/// in → `None` out (peer hasn't sent a Hello yet, or speaks an older
/// proto).
pub fn peer_commit_to_string(commit: Option<[u8; 8]>) -> Option<String> {
commit.and_then(|c| std::str::from_utf8(&c).ok().map(str::to_string))
}
#[derive(Default, Clone)] #[derive(Default, Clone)]
pub struct ClientData { pub struct ClientData {
pub handle: ClientHandle, pub handle: ClientHandle,
@@ -52,5 +43,4 @@ pub struct ClientData {
pub position: String, pub position: String,
pub resolving: bool, pub resolving: bool,
pub ips: Vec<String>, pub ips: Vec<String>,
pub peer_commit: Option<String>,
} }

View File

@@ -19,7 +19,6 @@ pub struct ClientObject {
#[property(name = "position", get, set, type = String, member = position)] #[property(name = "position", get, set, type = String, member = position)]
#[property(name = "resolving", get, set, type = bool, member = resolving)] #[property(name = "resolving", get, set, type = bool, member = resolving)]
#[property(name = "ips", get, set, type = Vec<String>, member = ips)] #[property(name = "ips", get, set, type = Vec<String>, member = ips)]
#[property(name = "peer-commit", get, set, type = Option<String>, member = peer_commit)]
pub data: RefCell<ClientData>, pub data: RefCell<ClientData>,
} }

View File

@@ -123,12 +123,6 @@ impl ClientRow {
bindings.push(position_binding); bindings.push(position_binding);
bindings.push(resolve_binding); bindings.push(resolve_binding);
bindings.push(ip_binding); bindings.push(ip_binding);
// Render the initial collapsed subtitle from whatever
// peer_commit the ClientObject was created with. Subsequent
// changes are pushed by `Window::update_client_state` calling
// `refresh_version_status` after writing the new property.
self.refresh_version_status();
} }
pub fn unbind(&self) { pub fn unbind(&self) {
@@ -156,40 +150,4 @@ impl ClientRow {
pub fn set_dns_state(&self, resolved: bool) { pub fn set_dns_state(&self, resolved: bool) {
self.imp().set_dns_state(resolved); self.imp().set_dns_state(resolved);
} }
/// Recompute the collapsed subtitle (Pango markup) based on the
/// current `peer-commit` property and the local build's commit.
/// Soft-warn semantics: a missing or mismatched peer commit
/// surfaces as orange text but never blocks traffic. Called by
/// the window after `update_client_state` writes the new
/// `peer-commit`. The dns-status icon is left to its existing
/// `set_dns_state` handler so the two indicators don't fight
/// for the same CSS class.
pub fn refresh_version_status(&self) {
let peer: Option<String> = self
.imp()
.client_object
.borrow()
.as_ref()
.and_then(|co| co.property::<Option<String>>("peer-commit"));
let local = crate::local_commit_str();
let markup = match peer.as_deref() {
None => format!("Peer version: unknown · Ours: {local}"),
Some(p) if p == local.as_str() => {
format!("Peer version: {p} · matched")
}
Some(p) => {
format!("Peer version: {p} · Ours: {local}")
}
};
self.remove_css_class("peer-mismatch");
self.remove_css_class("peer-match");
self.remove_css_class("peer-unknown");
match peer.as_deref() {
Some(p) if p == local.as_str() => self.add_css_class("peer-match"),
Some(_) => self.add_css_class("peer-mismatch"),
None => self.add_css_class("peer-unknown"),
};
self.set_subtitle(&markup);
}
} }

View File

@@ -10,29 +10,11 @@ mod macos_privacy;
mod macos_status_item; mod macos_status_item;
mod window; mod window;
use std::{env, process, str, sync::OnceLock}; use std::{env, process, str};
use gtk::CssProvider;
use window::Window; use window::Window;
/// Local build's commit hash, set once by [`run`] before the GTK use lan_mouse_ipc::{FrontendEvent, FrontendRequest, WindowIdentifier};
/// main loop starts. Read by per-row UI to compare against each
/// peer's [`lan_mouse_ipc::ClientState::peer_commit`] for the
/// soft-warn version-mismatch indicator.
pub(crate) static LOCAL_COMMIT: OnceLock<[u8; 8]> = OnceLock::new();
/// Convenience: returns the local commit as an 8-char ASCII string,
/// or a placeholder if unset (which would indicate a programmer
/// error since [`run`] always sets it).
pub(crate) fn local_commit_str() -> String {
LOCAL_COMMIT
.get()
.and_then(|c| std::str::from_utf8(c).ok())
.unwrap_or("????????")
.to_string()
}
use lan_mouse_ipc::FrontendEvent;
use adw::Application; use adw::Application;
use gtk::{IconTheme, gdk::Display, glib::clone, prelude::*}; use gtk::{IconTheme, gdk::Display, glib::clone, prelude::*};
@@ -41,6 +23,9 @@ use gtk::{gio, glib, prelude::ApplicationExt};
use self::client_object::ClientObject; use self::client_object::ClientObject;
use self::key_object::KeyObject; use self::key_object::KeyObject;
#[cfg(all(unix, feature = "wayland_window_identifier", not(target_os = "macos")))]
use gdk4_wayland::WaylandToplevel;
use thiserror::Error; use thiserror::Error;
#[derive(Error, Debug)] #[derive(Error, Debug)]
@@ -49,12 +34,8 @@ pub enum GtkError {
NonZeroExitCode(i32), NonZeroExitCode(i32),
} }
pub fn run(local_commit: [u8; 8]) -> Result<(), GtkError> { pub fn run() -> Result<(), GtkError> {
log::debug!("running gtk frontend"); log::debug!("running gtk frontend");
LOCAL_COMMIT
.set(local_commit)
.expect("local_commit set once");
#[cfg(windows)] #[cfg(windows)]
let ret = std::thread::Builder::new() let ret = std::thread::Builder::new()
.stack_size(8 * 1024 * 1024) // https://gitlab.gnome.org/GNOME/gtk/-/commit/52dbb3f372b2c3ea339e879689c1de535ba2c2c3 -> caused crash on windows .stack_size(8 * 1024 * 1024) // https://gitlab.gnome.org/GNOME/gtk/-/commit/52dbb3f372b2c3ea339e879689c1de535ba2c2c3 -> caused crash on windows
@@ -86,7 +67,6 @@ fn gtk_main() -> glib::ExitCode {
.build(); .build();
app.connect_startup(|app| { app.connect_startup(|app| {
load_css();
load_icons(); load_icons();
setup_actions(app); setup_actions(app);
setup_menu(app); setup_menu(app);
@@ -155,16 +135,6 @@ fn configure_macos_bundle_environment() {
); );
} }
fn load_css() {
let provider = CssProvider::default();
provider.load_from_resource("de/feschber/LanMouse/style.css");
gtk::style_context_add_provider_for_display(
&Display::default().expect("Could not connect to a display"),
&provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}
fn load_icons() { fn load_icons() {
let display = &Display::default().expect("Could not connect to a display."); let display = &Display::default().expect("Could not connect to a display.");
let icon_theme = IconTheme::for_display(display); let icon_theme = IconTheme::for_display(display);
@@ -260,6 +230,28 @@ fn build_ui(app: &Application) {
}); });
} }
// export TopLevel handle and send it to the service so that it can put the InpuCapture / RemoteDesktop
// windows on top of it using xdg-foreign.
#[cfg(all(unix, feature = "wayland_window_identifier", not(target_os = "macos")))]
window.connect_show(|window| {
// needs the surface so we have to present first!
if let Some(surface) = window.surface() {
if surface.display().backend().is_wayland() {
// let surface = surface.downcast::<WaylandSurface>();
let toplevel = surface.downcast::<WaylandToplevel>().expect("xdg-toplevel");
let window = window.clone();
toplevel.export_handle(move |_toplevel, handle| {
if let Ok(handle) = handle {
let handle = handle.to_string();
window.request(FrontendRequest::WindowIdentifier(
WindowIdentifier::Wayland(handle),
));
}
});
}
}
});
glib::spawn_future_local(clone!( glib::spawn_future_local(clone!(
#[weak] #[weak]
window, window,

View File

@@ -365,13 +365,6 @@ impl Window {
.map(|ip| ip.to_string()) .map(|ip| ip.to_string())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
client_object.set_ips(ips); client_object.set_ips(ips);
/* peer build version (drives the version-match indicator) */
client_object.set_property(
"peer-commit",
crate::client_object::peer_commit_to_string(state.peer_commit),
);
row.refresh_version_status();
} }
fn client_object_for_handle(&self, handle: ClientHandle) -> Option<ClientObject> { fn client_object_for_handle(&self, handle: ClientHandle) -> Option<ClientObject> {
@@ -440,7 +433,7 @@ impl Window {
self.request(FrontendRequest::RemoveAuthorizedKey(fp)); self.request(FrontendRequest::RemoveAuthorizedKey(fp));
} }
fn request(&self, request: FrontendRequest) { pub(crate) fn request(&self, request: FrontendRequest) {
let mut requester = self.imp().frontend_request_writer.borrow_mut(); let mut requester = self.imp().frontend_request_writer.borrow_mut();
let requester = requester.as_mut().unwrap(); let requester = requester.as_mut().unwrap();
if let Err(e) = requester.request(request) { if let Err(e) = requester.request(request) {

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "lan-mouse-ipc" name = "lan-mouse-ipc"
description = "library for communication between lan-mouse service and frontends" description = "library for communication between lan-mouse service and frontends"
version = "0.3.0" version = "0.2.0"
edition = "2021" edition = "2021"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse" repository = "https://github.com/feschber/lan-mouse"

View File

@@ -176,12 +176,6 @@ pub struct ClientState {
pub has_pressed_keys: bool, pub has_pressed_keys: bool,
/// dns resolving in progress /// dns resolving in progress
pub resolving: bool, pub resolving: bool,
/// Peer's build short commit hash from the [`Hello`] proto
/// event. `None` means we haven't received a Hello yet — either
/// the connection is fresh, or the peer is on an older build
/// that predates the Hello event. The frontend uses this to
/// soft-warn on version mismatch.
pub peer_commit: Option<[u8; 8]>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -261,6 +255,14 @@ pub enum FrontendRequest {
UpdateEnterHook(u64, Option<String>), UpdateEnterHook(u64, Option<String>),
/// save config file /// save config file
SaveConfiguration, SaveConfiguration,
/// window identifier used to present input-capture / remote-desktop prompts
WindowIdentifier(WindowIdentifier),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum WindowIdentifier {
Wayland(String),
X11(u32),
} }
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "lan-mouse-proto" name = "lan-mouse-proto"
description = "network protocol for lan-mouse" description = "network protocol for lan-mouse"
version = "0.3.0" version = "0.2.0"
edition = "2021" edition = "2021"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse" repository = "https://github.com/feschber/lan-mouse"
@@ -9,5 +9,5 @@ repository = "https://github.com/feschber/lan-mouse"
[dependencies] [dependencies]
num_enum = "0.7.2" num_enum = "0.7.2"
thiserror = "2.0.0" thiserror = "2.0.0"
input-event = { path = "../input-event", version = "0.4.0" } input-event = { path = "../input-event", version = "0.3.0" }
paste = "1.0" paste = "1.0"

View File

@@ -63,15 +63,6 @@ pub enum ProtoEvent {
Ping, Ping,
/// Response to [`ProtoEvent::Ping`], true if emulation is enabled / available /// Response to [`ProtoEvent::Ping`], true if emulation is enabled / available
Pong(bool), Pong(bool),
/// Build identification for the sending peer. Sent by the
/// connect side once after the connection authenticates, and
/// echoed back by the listen side in reply, so each end can
/// display the peer's build hash and warn (soft) on mismatch.
/// `commit` is the 8-byte ASCII short commit hash from
/// `shadow_rs`'s `SHORT_COMMIT`. Old peers that don't
/// recognize the event type silently skip it per the
/// forward-compat handling in the receive loop.
Hello { commit: [u8; 8] },
} }
impl Display for ProtoEvent { impl Display for ProtoEvent {
@@ -89,10 +80,6 @@ impl Display for ProtoEvent {
if *alive { "alive" } else { "not available" } if *alive { "alive" } else { "not available" }
) )
} }
ProtoEvent::Hello { commit } => {
let s = std::str::from_utf8(commit).unwrap_or("????????");
write!(f, "Hello({s})")
}
} }
} }
} }
@@ -111,7 +98,6 @@ pub enum EventType {
Enter, Enter,
Leave, Leave,
Ack, Ack,
Hello,
} }
impl ProtoEvent { impl ProtoEvent {
@@ -134,7 +120,6 @@ impl ProtoEvent {
ProtoEvent::Enter(_) => EventType::Enter, ProtoEvent::Enter(_) => EventType::Enter,
ProtoEvent::Leave(_) => EventType::Leave, ProtoEvent::Leave(_) => EventType::Leave,
ProtoEvent::Ack(_) => EventType::Ack, ProtoEvent::Ack(_) => EventType::Ack,
ProtoEvent::Hello { .. } => EventType::Hello,
} }
} }
} }
@@ -189,13 +174,6 @@ impl TryFrom<[u8; MAX_EVENT_SIZE]> for ProtoEvent {
EventType::Enter => Ok(Self::Enter(decode_u8(&mut buf)?.try_into()?)), EventType::Enter => Ok(Self::Enter(decode_u8(&mut buf)?.try_into()?)),
EventType::Leave => Ok(Self::Leave(decode_u32(&mut buf)?)), EventType::Leave => Ok(Self::Leave(decode_u32(&mut buf)?)),
EventType::Ack => Ok(Self::Ack(decode_u32(&mut buf)?)), EventType::Ack => Ok(Self::Ack(decode_u32(&mut buf)?)),
EventType::Hello => {
let mut commit = [0u8; 8];
for b in commit.iter_mut() {
*b = decode_u8(&mut buf)?;
}
Ok(Self::Hello { commit })
}
} }
} }
} }
@@ -260,11 +238,6 @@ impl From<ProtoEvent> for ([u8; MAX_EVENT_SIZE], usize) {
ProtoEvent::Enter(pos) => encode_u8(buf, len, pos as u8), ProtoEvent::Enter(pos) => encode_u8(buf, len, pos as u8),
ProtoEvent::Leave(serial) => encode_u32(buf, len, serial), ProtoEvent::Leave(serial) => encode_u32(buf, len, serial),
ProtoEvent::Ack(serial) => encode_u32(buf, len, serial), ProtoEvent::Ack(serial) => encode_u32(buf, len, serial),
ProtoEvent::Hello { commit } => {
for b in commit.iter() {
encode_u8(buf, len, *b);
}
}
} }
} }
(buf, len) (buf, len)

View File

@@ -1,12 +1,14 @@
use std::{ use std::{
cell::{Cell, RefCell}, cell::{Cell, RefCell},
rc::Rc, rc::Rc,
sync::{Arc, Mutex},
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use futures::StreamExt; use futures::StreamExt;
use input_capture::{ use input_capture::{
CaptureError, CaptureEvent, CaptureHandle, InputCapture, InputCaptureError, Position, CaptureError, CaptureEvent, CaptureHandle, InputCapture, InputCaptureError, Position,
WindowIdentifier,
}; };
use input_event::{Event, KeyboardEvent, scancode}; use input_event::{Event, KeyboardEvent, scancode};
use lan_mouse_proto::ProtoEvent; use lan_mouse_proto::ProtoEvent;
@@ -68,6 +70,7 @@ impl Capture {
backend: Option<input_capture::Backend>, backend: Option<input_capture::Backend>,
conn: LanMouseConnection, conn: LanMouseConnection,
release_bind: Vec<scancode::Linux>, release_bind: Vec<scancode::Linux>,
window_identifier: Arc<Mutex<Option<WindowIdentifier>>>,
) -> Self { ) -> Self {
let (request_tx, request_rx) = channel(); let (request_tx, request_rx) = channel();
let (event_tx, event_rx) = channel(); let (event_tx, event_rx) = channel();
@@ -82,6 +85,7 @@ impl Capture {
request_rx, request_rx,
release_bind: Rc::new(RefCell::new(release_bind)), release_bind: Rc::new(RefCell::new(release_bind)),
state: Default::default(), state: Default::default(),
window_identifier,
}; };
let task = spawn_local(capture_task.run()); let task = spawn_local(capture_task.run());
Self { Self {
@@ -166,6 +170,7 @@ struct CaptureTask {
release_bind: Rc<RefCell<Vec<scancode::Linux>>>, release_bind: Rc<RefCell<Vec<scancode::Linux>>>,
request_rx: Receiver<CaptureRequest>, request_rx: Receiver<CaptureRequest>,
state: State, state: State,
window_identifier: Arc<Mutex<Option<WindowIdentifier>>>,
} }
impl CaptureTask { impl CaptureTask {
@@ -200,6 +205,7 @@ impl CaptureTask {
} }
async fn run(mut self) { async fn run(mut self) {
tokio::time::sleep(Duration::from_secs(1)).await;
loop { loop {
if let Err(e) = self.do_capture().await { if let Err(e) = self.do_capture().await {
log::warn!("input capture exited: {e}"); log::warn!("input capture exited: {e}");
@@ -224,7 +230,7 @@ impl CaptureTask {
async fn do_capture(&mut self) -> Result<(), InputCaptureError> { async fn do_capture(&mut self) -> Result<(), InputCaptureError> {
/* allow cancelling capture request */ /* allow cancelling capture request */
let mut capture = tokio::select! { let mut capture = tokio::select! {
r = InputCapture::new(self.backend) => r?, r = InputCapture::new(self.backend, self.window_identifier.clone()) => r?,
_ = self.cancellation_token.cancelled() => return Ok(()), _ = self.cancellation_token.cancelled() => return Ok(()),
}; };

View File

@@ -1,3 +1,5 @@
use std::sync::{Arc, Mutex};
use crate::config::Config; use crate::config::Config;
use clap::Args; use clap::Args;
use futures::StreamExt; use futures::StreamExt;
@@ -12,7 +14,7 @@ pub async fn run(config: Config, _args: TestCaptureArgs) -> Result<(), InputCapt
log::info!("creating input capture"); log::info!("creating input capture");
let backend = config.capture_backend().map(|b| b.into()); let backend = config.capture_backend().map(|b| b.into());
loop { loop {
let mut input_capture = InputCapture::new(backend).await?; let mut input_capture = InputCapture::new(backend, Arc::new(Mutex::new(None))).await?;
log::info!("creating clients"); log::info!("creating clients");
input_capture.create(0, Position::Left).await?; input_capture.create(0, Position::Left).await?;
input_capture.create(4, Position::Left).await?; input_capture.create(4, Position::Left).await?;

View File

@@ -282,12 +282,6 @@ impl ClientManager {
} }
} }
pub(crate) fn set_peer_commit(&self, handle: ClientHandle, commit: Option<[u8; 8]>) {
if let Some((_, s)) = self.clients.borrow_mut().get_mut(handle as usize) {
s.peer_commit = commit;
}
}
pub(crate) fn active_addr(&self, handle: ClientHandle) -> Option<SocketAddr> { pub(crate) fn active_addr(&self, handle: ClientHandle) -> Option<SocketAddr> {
self.clients self.clients
.borrow() .borrow()

View File

@@ -1,7 +1,6 @@
use crate::capture_test::TestCaptureArgs; use crate::capture_test::TestCaptureArgs;
use crate::emulation_test::TestEmulationArgs; use crate::emulation_test::TestEmulationArgs;
use clap::{Parser, Subcommand, ValueEnum}; use clap::{Parser, Subcommand, ValueEnum};
use notify::event::ModifyKind;
use notify::{EventKind, RecommendedWatcher, Watcher}; use notify::{EventKind, RecommendedWatcher, Watcher};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
@@ -28,18 +27,6 @@ use shadow_rs::shadow;
shadow!(build); shadow!(build);
/// Local build's 8-byte ASCII short commit hash, suitable for use
/// in [`lan_mouse_proto::ProtoEvent::Hello`]. Pads with `'?'` if
/// shadow_rs returns an unexpected length so the field is always
/// well-formed on the wire.
pub fn local_commit() -> [u8; 8] {
let bytes = build::SHORT_COMMIT.as_bytes();
let mut out = [b'?'; 8];
let n = bytes.len().min(8);
out[..n].copy_from_slice(&bytes[..n]);
out
}
const CONFIG_FILE_NAME: &str = "config.toml"; const CONFIG_FILE_NAME: &str = "config.toml";
const CERT_FILE_NAME: &str = "lan-mouse.pem"; const CERT_FILE_NAME: &str = "lan-mouse.pem";
@@ -131,13 +118,13 @@ pub enum Command {
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)] #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
pub enum CaptureBackend { pub enum CaptureBackend {
#[cfg(libei_capture)] #[cfg(all(unix, feature = "libei_capture", not(target_os = "macos")))]
#[serde(rename = "input-capture-portal")] #[serde(rename = "input-capture-portal")]
InputCapturePortal, InputCapturePortal,
#[cfg(layer_shell_capture)] #[cfg(all(unix, feature = "layer_shell_capture", not(target_os = "macos")))]
#[serde(rename = "layer-shell")] #[serde(rename = "layer-shell")]
LayerShell, LayerShell,
#[cfg(x11_capture)] #[cfg(all(unix, feature = "x11_capture", not(target_os = "macos")))]
#[serde(rename = "x11")] #[serde(rename = "x11")]
X11, X11,
#[cfg(windows)] #[cfg(windows)]
@@ -153,11 +140,11 @@ pub enum CaptureBackend {
impl Display for CaptureBackend { impl Display for CaptureBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
#[cfg(libei_capture)] #[cfg(all(unix, feature = "libei_capture", not(target_os = "macos")))]
CaptureBackend::InputCapturePortal => write!(f, "input-capture-portal"), CaptureBackend::InputCapturePortal => write!(f, "input-capture-portal"),
#[cfg(layer_shell_capture)] #[cfg(all(unix, feature = "layer_shell_capture", not(target_os = "macos")))]
CaptureBackend::LayerShell => write!(f, "layer-shell"), CaptureBackend::LayerShell => write!(f, "layer-shell"),
#[cfg(x11_capture)] #[cfg(all(unix, feature = "x11_capture", not(target_os = "macos")))]
CaptureBackend::X11 => write!(f, "X11"), CaptureBackend::X11 => write!(f, "X11"),
#[cfg(windows)] #[cfg(windows)]
CaptureBackend::Windows => write!(f, "windows"), CaptureBackend::Windows => write!(f, "windows"),
@@ -171,11 +158,11 @@ impl Display for CaptureBackend {
impl From<CaptureBackend> for input_capture::Backend { impl From<CaptureBackend> for input_capture::Backend {
fn from(backend: CaptureBackend) -> Self { fn from(backend: CaptureBackend) -> Self {
match backend { match backend {
#[cfg(libei_capture)] #[cfg(all(unix, feature = "libei_capture", not(target_os = "macos")))]
CaptureBackend::InputCapturePortal => Self::InputCapturePortal, CaptureBackend::InputCapturePortal => Self::InputCapturePortal,
#[cfg(layer_shell_capture)] #[cfg(all(unix, feature = "layer_shell_capture", not(target_os = "macos")))]
CaptureBackend::LayerShell => Self::LayerShell, CaptureBackend::LayerShell => Self::LayerShell,
#[cfg(x11_capture)] #[cfg(all(unix, feature = "x11_capture", not(target_os = "macos")))]
CaptureBackend::X11 => Self::X11, CaptureBackend::X11 => Self::X11,
#[cfg(windows)] #[cfg(windows)]
CaptureBackend::Windows => Self::Windows, CaptureBackend::Windows => Self::Windows,
@@ -188,16 +175,16 @@ impl From<CaptureBackend> for input_capture::Backend {
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)] #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
pub enum EmulationBackend { pub enum EmulationBackend {
#[cfg(wlroots_emulation)] #[cfg(all(unix, feature = "wlroots_emulation", not(target_os = "macos")))]
#[serde(rename = "wlroots")] #[serde(rename = "wlroots")]
Wlroots, Wlroots,
#[cfg(libei_emulation)] #[cfg(all(unix, feature = "libei_emulation", not(target_os = "macos")))]
#[serde(rename = "libei")] #[serde(rename = "libei")]
Libei, Libei,
#[cfg(rdp_emulation)] #[cfg(all(unix, feature = "rdp_emulation", not(target_os = "macos")))]
#[serde(rename = "xdp")] #[serde(rename = "xdp")]
Xdp, Xdp,
#[cfg(x11_emulation)] #[cfg(all(unix, feature = "x11_emulation", not(target_os = "macos")))]
#[serde(rename = "x11")] #[serde(rename = "x11")]
X11, X11,
#[cfg(windows)] #[cfg(windows)]
@@ -213,13 +200,13 @@ pub enum EmulationBackend {
impl From<EmulationBackend> for input_emulation::Backend { impl From<EmulationBackend> for input_emulation::Backend {
fn from(backend: EmulationBackend) -> Self { fn from(backend: EmulationBackend) -> Self {
match backend { match backend {
#[cfg(wlroots_emulation)] #[cfg(all(unix, feature = "wlroots_emulation", not(target_os = "macos")))]
EmulationBackend::Wlroots => Self::Wlroots, EmulationBackend::Wlroots => Self::Wlroots,
#[cfg(libei_emulation)] #[cfg(all(unix, feature = "libei_emulation", not(target_os = "macos")))]
EmulationBackend::Libei => Self::Libei, EmulationBackend::Libei => Self::Libei,
#[cfg(rdp_emulation)] #[cfg(all(unix, feature = "rdp_emulation", not(target_os = "macos")))]
EmulationBackend::Xdp => Self::Xdp, EmulationBackend::Xdp => Self::Xdp,
#[cfg(x11_emulation)] #[cfg(all(unix, feature = "x11_emulation", not(target_os = "macos")))]
EmulationBackend::X11 => Self::X11, EmulationBackend::X11 => Self::X11,
#[cfg(windows)] #[cfg(windows)]
EmulationBackend::Windows => Self::Windows, EmulationBackend::Windows => Self::Windows,
@@ -233,13 +220,13 @@ impl From<EmulationBackend> for input_emulation::Backend {
impl Display for EmulationBackend { impl Display for EmulationBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
#[cfg(wlroots_emulation)] #[cfg(all(unix, feature = "wlroots_emulation", not(target_os = "macos")))]
EmulationBackend::Wlroots => write!(f, "wlroots"), EmulationBackend::Wlroots => write!(f, "wlroots"),
#[cfg(libei_emulation)] #[cfg(all(unix, feature = "libei_emulation", not(target_os = "macos")))]
EmulationBackend::Libei => write!(f, "libei"), EmulationBackend::Libei => write!(f, "libei"),
#[cfg(rdp_emulation)] #[cfg(all(unix, feature = "rdp_emulation", not(target_os = "macos")))]
EmulationBackend::Xdp => write!(f, "xdg-desktop-portal"), EmulationBackend::Xdp => write!(f, "xdg-desktop-portal"),
#[cfg(x11_emulation)] #[cfg(all(unix, feature = "x11_emulation", not(target_os = "macos")))]
EmulationBackend::X11 => write!(f, "X11"), EmulationBackend::X11 => write!(f, "X11"),
#[cfg(windows)] #[cfg(windows)]
EmulationBackend::Windows => write!(f, "windows"), EmulationBackend::Windows => write!(f, "windows"),
@@ -419,9 +406,7 @@ impl Config {
if event.paths.contains(&self.config_path) if event.paths.contains(&self.config_path)
&& matches!( && matches!(
event.kind, event.kind,
EventKind::Create(_) EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_)
| EventKind::Modify(ModifyKind::Data(_))
| EventKind::Remove(_)
) )
&& self.read_from_disk()? && self.read_from_disk()?
{ {
@@ -539,11 +524,6 @@ impl Config {
} }
Err(e) => log::warn!("{:?} {e}", self.config_path()), Err(e) => log::warn!("{:?} {e}", self.config_path()),
}; };
if changed {
log::info!("config changed");
} else {
log::info!("config unchanged");
}
Ok(changed) Ok(changed)
} }

View File

@@ -1,5 +1,4 @@
use crate::client::ClientManager; use crate::client::ClientManager;
use crate::config::local_commit;
use lan_mouse_ipc::{ClientHandle, DEFAULT_PORT}; use lan_mouse_ipc::{ClientHandle, DEFAULT_PORT};
use lan_mouse_proto::{MAX_EVENT_SIZE, ProtoEvent}; use lan_mouse_proto::{MAX_EVENT_SIZE, ProtoEvent};
use local_channel::mpsc::{Receiver, Sender, channel}; use local_channel::mpsc::{Receiver, Sender, channel};
@@ -198,19 +197,6 @@ async fn connect_to_handle(
conns.lock().await.insert(addr, conn.clone()); conns.lock().await.insert(addr, conn.clone());
connecting.lock().await.remove(&handle); connecting.lock().await.remove(&handle);
// Best-effort version handshake. Send our commit hash once
// immediately after the DTLS handshake; the listen side
// mirrors a Hello back so the receive loop can populate
// `peer_commit`. Old peers will silently skip this event
// per the forward-compat handler in [`receive_loop`].
let (buf, len) = ProtoEvent::Hello {
commit: local_commit(),
}
.into();
if let Err(e) = conn.send(&buf[..len]).await {
log::debug!("hello send to {addr} failed: {e}");
}
// poll connection for active // poll connection for active
spawn_local(ping_pong(addr, conn.clone(), ping_response.clone())); spawn_local(ping_pong(addr, conn.clone(), ping_response.clone()));
@@ -269,26 +255,16 @@ async fn receive_loop(
) { ) {
let mut buf = [0u8; MAX_EVENT_SIZE]; let mut buf = [0u8; MAX_EVENT_SIZE];
while conn.recv(&mut buf).await.is_ok() { while conn.recv(&mut buf).await.is_ok() {
match buf.try_into() { if let Ok(event) = buf.try_into() {
Ok(event) => { log::trace!("{addr} <==<==<== {event}");
log::trace!("{addr} <==<==<== {event}"); match event {
match event { ProtoEvent::Pong(b) => {
ProtoEvent::Pong(b) => { client_manager.set_active_addr(handle, Some(addr));
client_manager.set_active_addr(handle, Some(addr)); client_manager.set_alive(handle, b);
client_manager.set_alive(handle, b); ping_response.borrow_mut().insert(addr);
ping_response.borrow_mut().insert(addr);
}
ProtoEvent::Hello { commit } => {
client_manager.set_peer_commit(handle, Some(commit));
}
event => tx.send((handle, event)).expect("channel closed"),
} }
event => tx.send((handle, event)).expect("channel closed"),
} }
// Skip undecodable datagrams without dropping the
// connection. Each DTLS recv is one framed message, so
// skipping is safe and keeps us forward-compatible with
// peers that send event types we don't yet know about.
Err(e) => log::debug!("ignoring undecodable event from {addr}: {e}"),
} }
} }
log::warn!("recv error"); log::warn!("recv error");
@@ -304,7 +280,6 @@ async fn disconnect(
log::warn!("client ({handle}) @ {addr} connection closed"); log::warn!("client ({handle}) @ {addr} connection closed");
conns.lock().await.remove(&addr); conns.lock().await.remove(&addr);
client_manager.set_active_addr(handle, None); client_manager.set_active_addr(handle, None);
client_manager.set_peer_commit(handle, None);
let active: Vec<SocketAddr> = conns.lock().await.keys().copied().collect(); let active: Vec<SocketAddr> = conns.lock().await.keys().copied().collect();
log::info!("active connections: {active:?}"); log::info!("active connections: {active:?}");
} }

View File

@@ -1,4 +1,3 @@
use crate::config::local_commit;
use crate::listen::{LanMouseListener, ListenEvent, ListenerCreationError}; use crate::listen::{LanMouseListener, ListenEvent, ListenerCreationError};
use futures::StreamExt; use futures::StreamExt;
use input_emulation::{EmulationHandle, InputEmulation, InputEmulationError}; use input_emulation::{EmulationHandle, InputEmulation, InputEmulationError};
@@ -53,17 +52,6 @@ pub(crate) enum EmulationEvent {
EmulationEnabled, EmulationEnabled,
/// capture should be released /// capture should be released
ReleaseNotify, ReleaseNotify,
/// peer sent us a Hello with its build commit hash. Used to
/// populate `client_manager.peer_commit` from the listen side
/// too — without this, peer-version visibility silently fails
/// whenever the outgoing connection in the *other* direction is
/// broken (one-way setups, asymmetric NAT, peer's TCP listener
/// down). The connect-side path stays as the primary source;
/// this is the defensive fallback.
PeerHello {
addr: SocketAddr,
commit: [u8; 8],
},
} }
enum EmulationRequest { enum EmulationRequest {
@@ -162,21 +150,6 @@ impl ListenTask {
} }
ProtoEvent::Input(event) => self.emulation_proxy.consume(event, addr), ProtoEvent::Input(event) => self.emulation_proxy.consume(event, addr),
ProtoEvent::Ping => self.listener.reply(addr, ProtoEvent::Pong(self.emulation_proxy.emulation_active.get())).await, ProtoEvent::Ping => self.listener.reply(addr, ProtoEvent::Pong(self.emulation_proxy.emulation_active.get())).await,
// Peer's version handshake. Echo our own
// commit back so the peer's connect-side
// receive_loop populates its `peer_commit`,
// AND publish a PeerHello upward so our
// service can populate ours from the listen
// side too — the connect side is the primary
// path, but if the outbound direction is
// broken (one-way setup, NAT, peer's TCP
// listener down) the version display would
// otherwise silently say "unknown" while
// the peer is in fact happily talking to us.
ProtoEvent::Hello { commit } => {
self.listener.reply(addr, ProtoEvent::Hello { commit: local_commit() }).await;
self.event_tx.send(EmulationEvent::PeerHello { addr, commit }).expect("channel closed");
}
_ => {} _ => {}
} }
} }

View File

@@ -259,16 +259,8 @@ async fn read_loop(
.send(ListenEvent::Msg { event, addr }) .send(ListenEvent::Msg { event, addr })
.expect("channel closed"), .expect("channel closed"),
Err(e) => { Err(e) => {
// Skip the malformed/unknown datagram and keep log::warn!("error receiving event: {e}");
// listening. Each DTLS recv returns one full break;
// datagram, so a parse error here can't desync a
// stream; the next call gets a fresh, framed
// message. This makes the protocol forward-
// compatible: a peer running a newer Lan Mouse
// version can introduce additional event types
// and old peers will simply ignore them rather
// than dropping the connection.
log::debug!("ignoring undecodable event from {addr}: {e}");
} }
} }
} }

View File

@@ -74,7 +74,7 @@ fn run() -> Result<(), LanMouseError> {
#[cfg(feature = "gtk")] #[cfg(feature = "gtk")]
{ {
let mut service = start_service()?; let mut service = start_service()?;
let res = lan_mouse_gtk::run(config::local_commit()); let res = lan_mouse_gtk::run();
#[cfg(unix)] #[cfg(unix)]
{ {
// on unix we give the service a chance to terminate gracefully // on unix we give the service a chance to terminate gracefully

View File

@@ -19,7 +19,7 @@ use std::{
collections::{HashMap, HashSet, VecDeque}, collections::{HashMap, HashSet, VecDeque},
io, io,
net::{IpAddr, SocketAddr}, net::{IpAddr, SocketAddr},
sync::{Arc, RwLock}, sync::{Arc, Mutex, RwLock},
}; };
use thiserror::Error; use thiserror::Error;
use tokio::{process::Command, signal, sync::Notify}; use tokio::{process::Command, signal, sync::Notify};
@@ -70,6 +70,7 @@ pub struct Service {
/// map from capture handle to connection info /// map from capture handle to connection info
incoming_conn_info: HashMap<ClientHandle, Incoming>, incoming_conn_info: HashMap<ClientHandle, Incoming>,
next_trigger_handle: u64, next_trigger_handle: u64,
window_identifier: Arc<Mutex<Option<input_capture::WindowIdentifier>>>,
} }
#[derive(Debug)] #[derive(Debug)]
@@ -101,7 +102,13 @@ impl Service {
// input capture + emulation // input capture + emulation
let capture_backend = config.capture_backend().map(|b| b.into()); let capture_backend = config.capture_backend().map(|b| b.into());
let capture = Capture::new(capture_backend, conn, config.release_bind()); let window_identifier = Arc::new(Mutex::new(None));
let capture = Capture::new(
capture_backend,
conn,
config.release_bind(),
window_identifier.clone(),
);
let emulation_backend = config.emulation_backend().map(|b| b.into()); let emulation_backend = config.emulation_backend().map(|b| b.into());
let emulation = Emulation::new(emulation_backend, listener); let emulation = Emulation::new(emulation_backend, listener);
@@ -126,6 +133,7 @@ impl Service {
incoming_conn_info: Default::default(), incoming_conn_info: Default::default(),
incoming_conns: Default::default(), incoming_conns: Default::default(),
next_trigger_handle: 0, next_trigger_handle: 0,
window_identifier,
}; };
Ok(service) Ok(service)
} }
@@ -218,6 +226,20 @@ impl Service {
self.update_enter_hook(handle, enter_hook) self.update_enter_hook(handle, enter_hook)
} }
FrontendRequest::SaveConfiguration => self.save_config(), FrontendRequest::SaveConfiguration => self.save_config(),
FrontendRequest::WindowIdentifier(handle) => {
log::info!("xdg-foreign handle: {handle:?}");
self.window_identifier
.lock()
.unwrap()
.replace(match handle {
lan_mouse_ipc::WindowIdentifier::Wayland(handle) => {
input_capture::WindowIdentifier::Wayland(handle)
}
lan_mouse_ipc::WindowIdentifier::X11(xid) => {
input_capture::WindowIdentifier::X11(xid)
}
});
}
} }
} }
@@ -319,17 +341,6 @@ impl Service {
EmulationEvent::Connected { addr, fingerprint } => { EmulationEvent::Connected { addr, fingerprint } => {
self.notify_frontend(FrontendEvent::DeviceConnected { addr, fingerprint }); self.notify_frontend(FrontendEvent::DeviceConnected { addr, fingerprint });
} }
EmulationEvent::PeerHello { addr, commit } => {
// Map the peer's source addr back to its client handle
// and stamp the commit. Skip if we don't have an
// outgoing client configured for this peer (incoming-
// only setup) — there's nowhere to display the version
// in that case anyway.
if let Some(handle) = self.client_manager.get_client(addr) {
self.client_manager.set_peer_commit(handle, Some(commit));
self.broadcast_client(handle);
}
}
} }
} }