Compare commits

..

9 Commits

Author SHA1 Message Date
Ty Smith
4b93be3228 docs(macos): clarify repeat-task cleanup releases the key
Address @feschber review feedback on PR #441. The repeat-task cleanup
already releases the key with the correct CGKeyCode via the existing
key_event call at the end of the closure — this commit just expands
the surrounding comment to make that explicit and to document why
update_modifiers is intentionally NOT called from this path (Mac
CGKeyCode vs Linux evdev scancode collision).

No behavioral change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-12 12:07:03 +02:00
Ty Smith
c32d695cd9 macos: stop corrupting modifier state in repeat-task cleanup
spawn_repeat_task() takes a Mac CGKeyCode, but the cleanup block was
passing that value to update_modifiers(), which expects a Linux evdev
scancode (it calls scancode::Linux::try_from(key)). The two codespaces
collide on several values, so cancelling the repeat task could
silently clear a still-held modifier:

  Mac LeftShift   = 56  == Linux KeyLeftAlt   = 56  -> clears Mod1Mask
  Mac Down arrow  = 125 == Linux KeyLeftMeta  = 125 -> clears Mod4Mask
  Mac Up arrow    = 126 == Linux KeyRightMeta = 126 -> clears Mod4Mask
  Mac Backslash   = 42  == Linux KeyLeftShift = 42  -> clears ShiftMask
  Mac "9"         = 29  == Linux KeyLeftCtrl  = 29  -> clears ControlMask

In practice this broke chords such as Shift+Option+X and Cmd+Down:
pressing Shift while holding Option cancels Option's repeat task and
runs the buggy cleanup, which then interprets Mac LeftShift's code
(56) as Linux KeyLeftAlt and removes Option from the modifier state.
The next key arrives with Shift only, so window-manager bindings on
the original Option chord never fire.

Remove the buggy update_modifiers() call. Modifier state is owned by
the main consume() loop, which already calls update_modifiers() with
the correct Linux scancode on the real release event.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-12 12:07:03 +02:00
Ty Smith
82d677f9c8 macos: post NumericPad and SecondaryFn flags for synthesized arrow keys
Hardware-generated arrow key events on macOS carry the NumericPad and
SecondaryFn flags in addition to any user-pressed modifiers. CGEventTap-
based hotkey matchers (tiling window managers, accessibility tools, etc.)
commonly check those flags to disambiguate navigation arrows from generic
chords, and reject events that lack them.

Before this change, synthesized Option+Arrow chords were silently
swallowed by the focused application instead of being captured by the
window manager, because the events arrived with only the Alternate flag
set. Hardware Option+Arrow chords on the local keyboard worked because
the OS itself set the missing flags.

Add NumericPad + SecondaryFn to the flags posted with arrow key events
(Mac key codes 0x7B-0x7E) so synthesized arrow chords match hardware
chords on the wire.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-12 12:07:03 +02:00
Ferdinand Schober
7ef43418c9 fix output name 2026-06-11 17:27:04 +02:00
Ferdinand Schober
8f32b7fe96 fix short-sha 2026-06-11 16:55:33 +02:00
Ferdinand Schober
02ac0bf220 include commit hash in pre-release (#456)
this solves the "create new release" issue as well as tag conflicts with
branch name
2026-06-11 16:50:04 +02:00
Ferdinand Schober
1b53e58ba9 remaining feature flags (#444) 2026-06-08 14:38:24 +02:00
Ferdinand Schober
a9461ae830 move feature flags to build.rs (#439) 2026-05-19 11:26:06 +02:00
Ferdinand Schober
1fa3800d3c windows: fix clippy lints 2026-05-16 17:09:06 +02:00
21 changed files with 258 additions and 262 deletions

View File

@@ -174,13 +174,16 @@ jobs:
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
- name: Get short SHA
id: vars
run: echo "short_sha=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
- name: Create Pre-Release
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
uses: softprops/action-gh-release@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag_name: ${{ github.event.inputs.name || github.ref_name }}
name: ${{ github.event.inputs.name || github.ref_name }}
tag_name: ${{ format('{0}-{1}', github.event.inputs.name || github.ref_name, steps.vars.outputs.short_sha) }}
name: ${{ format('{0}-{1}', github.event.inputs.name || github.ref_name, steps.vars.outputs.short_sha) }}
prerelease: true
generate_release_notes: true
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 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")))`).
- Feature flags live in root `Cargo.toml`. Gate OS-specific modules with the configs exported in build.rs (e.g., `cfg(layer_shell)`).
- 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.

25
Cargo.lock generated
View File

@@ -1087,30 +1087,6 @@ dependencies = [
"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]]
name = "generic-array"
version = "0.14.7"
@@ -1913,7 +1889,6 @@ name = "lan-mouse-gtk"
version = "0.2.0"
dependencies = [
"async-channel",
"gdk4-wayland",
"glib-build-tools",
"gtk4",
"hostname",

View File

@@ -5,4 +5,57 @@ fn main() {
.deny_const(Default::default())
.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");
}
}

25
input-capture/build.rs Normal file
View File

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

View File

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

View File

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

View File

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

31
input-emulation/build.rs Normal file
View File

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

View File

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

View File

@@ -106,8 +106,19 @@ impl MacOSEmulation {
}
}
}
// release key when cancelled
update_modifiers(&modifiers, key as u32, 0);
// Always release the key with the correct CGKeyCode, regardless of
// whether the repeat loop ran. This matches @feschber's review
// 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());
});
self.repeat_task = Some(repeat_task);
@@ -157,6 +168,19 @@ extern "C" {
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) {
let event = match CGEvent::new_keyboard_event(event_source, key, state != 0) {
Ok(e) => e,
@@ -165,7 +189,15 @@ fn key_event(event_source: CGEventSource, key: u16, state: u8, modifiers: XMods)
return;
}
};
event.set_flags(to_cgevent_flags(modifiers));
let mut 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);
log::trace!("key event: {key} {state}");
}

View File

@@ -15,13 +15,5 @@ log = "0.4.20"
lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.2.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]
glib-build-tools = { version = "0.20.0" }
[features]
default = ["wayland_window_identifier"]
wayland_window_identifier = ["dep:gdk4_wayland"]

View File

@@ -14,7 +14,7 @@ use std::{env, process, str};
use window::Window;
use lan_mouse_ipc::{FrontendEvent, FrontendRequest, WindowIdentifier};
use lan_mouse_ipc::FrontendEvent;
use adw::Application;
use gtk::{IconTheme, gdk::Display, glib::clone, prelude::*};
@@ -23,9 +23,6 @@ use gtk::{gio, glib, prelude::ApplicationExt};
use self::client_object::ClientObject;
use self::key_object::KeyObject;
#[cfg(all(unix, feature = "wayland_window_identifier", not(target_os = "macos")))]
use gdk4_wayland::WaylandToplevel;
use thiserror::Error;
#[derive(Error, Debug)]
@@ -230,28 +227,6 @@ 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!(
#[weak]
window,

View File

@@ -433,7 +433,7 @@ impl Window {
self.request(FrontendRequest::RemoveAuthorizedKey(fp));
}
pub(crate) fn request(&self, request: FrontendRequest) {
fn request(&self, request: FrontendRequest) {
let mut requester = self.imp().frontend_request_writer.borrow_mut();
let requester = requester.as_mut().unwrap();
if let Err(e) = requester.request(request) {

View File

@@ -255,14 +255,6 @@ pub enum FrontendRequest {
UpdateEnterHook(u64, Option<String>),
/// save config file
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)]

View File

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

View File

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

View File

@@ -118,13 +118,13 @@ pub enum Command {
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
pub enum CaptureBackend {
#[cfg(all(unix, feature = "libei_capture", not(target_os = "macos")))]
#[cfg(libei_capture)]
#[serde(rename = "input-capture-portal")]
InputCapturePortal,
#[cfg(all(unix, feature = "layer_shell_capture", not(target_os = "macos")))]
#[cfg(layer_shell_capture)]
#[serde(rename = "layer-shell")]
LayerShell,
#[cfg(all(unix, feature = "x11_capture", not(target_os = "macos")))]
#[cfg(x11_capture)]
#[serde(rename = "x11")]
X11,
#[cfg(windows)]
@@ -140,11 +140,11 @@ pub enum CaptureBackend {
impl Display for CaptureBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#[cfg(all(unix, feature = "libei_capture", not(target_os = "macos")))]
#[cfg(libei_capture)]
CaptureBackend::InputCapturePortal => write!(f, "input-capture-portal"),
#[cfg(all(unix, feature = "layer_shell_capture", not(target_os = "macos")))]
#[cfg(layer_shell_capture)]
CaptureBackend::LayerShell => write!(f, "layer-shell"),
#[cfg(all(unix, feature = "x11_capture", not(target_os = "macos")))]
#[cfg(x11_capture)]
CaptureBackend::X11 => write!(f, "X11"),
#[cfg(windows)]
CaptureBackend::Windows => write!(f, "windows"),
@@ -158,11 +158,11 @@ impl Display for CaptureBackend {
impl From<CaptureBackend> for input_capture::Backend {
fn from(backend: CaptureBackend) -> Self {
match backend {
#[cfg(all(unix, feature = "libei_capture", not(target_os = "macos")))]
#[cfg(libei_capture)]
CaptureBackend::InputCapturePortal => Self::InputCapturePortal,
#[cfg(all(unix, feature = "layer_shell_capture", not(target_os = "macos")))]
#[cfg(layer_shell_capture)]
CaptureBackend::LayerShell => Self::LayerShell,
#[cfg(all(unix, feature = "x11_capture", not(target_os = "macos")))]
#[cfg(x11_capture)]
CaptureBackend::X11 => Self::X11,
#[cfg(windows)]
CaptureBackend::Windows => Self::Windows,
@@ -175,16 +175,16 @@ impl From<CaptureBackend> for input_capture::Backend {
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
pub enum EmulationBackend {
#[cfg(all(unix, feature = "wlroots_emulation", not(target_os = "macos")))]
#[cfg(wlroots_emulation)]
#[serde(rename = "wlroots")]
Wlroots,
#[cfg(all(unix, feature = "libei_emulation", not(target_os = "macos")))]
#[cfg(libei_emulation)]
#[serde(rename = "libei")]
Libei,
#[cfg(all(unix, feature = "rdp_emulation", not(target_os = "macos")))]
#[cfg(rdp_emulation)]
#[serde(rename = "xdp")]
Xdp,
#[cfg(all(unix, feature = "x11_emulation", not(target_os = "macos")))]
#[cfg(x11_emulation)]
#[serde(rename = "x11")]
X11,
#[cfg(windows)]
@@ -200,13 +200,13 @@ pub enum EmulationBackend {
impl From<EmulationBackend> for input_emulation::Backend {
fn from(backend: EmulationBackend) -> Self {
match backend {
#[cfg(all(unix, feature = "wlroots_emulation", not(target_os = "macos")))]
#[cfg(wlroots_emulation)]
EmulationBackend::Wlroots => Self::Wlroots,
#[cfg(all(unix, feature = "libei_emulation", not(target_os = "macos")))]
#[cfg(libei_emulation)]
EmulationBackend::Libei => Self::Libei,
#[cfg(all(unix, feature = "rdp_emulation", not(target_os = "macos")))]
#[cfg(rdp_emulation)]
EmulationBackend::Xdp => Self::Xdp,
#[cfg(all(unix, feature = "x11_emulation", not(target_os = "macos")))]
#[cfg(x11_emulation)]
EmulationBackend::X11 => Self::X11,
#[cfg(windows)]
EmulationBackend::Windows => Self::Windows,
@@ -220,13 +220,13 @@ impl From<EmulationBackend> for input_emulation::Backend {
impl Display for EmulationBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#[cfg(all(unix, feature = "wlroots_emulation", not(target_os = "macos")))]
#[cfg(wlroots_emulation)]
EmulationBackend::Wlroots => write!(f, "wlroots"),
#[cfg(all(unix, feature = "libei_emulation", not(target_os = "macos")))]
#[cfg(libei_emulation)]
EmulationBackend::Libei => write!(f, "libei"),
#[cfg(all(unix, feature = "rdp_emulation", not(target_os = "macos")))]
#[cfg(rdp_emulation)]
EmulationBackend::Xdp => write!(f, "xdg-desktop-portal"),
#[cfg(all(unix, feature = "x11_emulation", not(target_os = "macos")))]
#[cfg(x11_emulation)]
EmulationBackend::X11 => write!(f, "X11"),
#[cfg(windows)]
EmulationBackend::Windows => write!(f, "windows"),

View File

@@ -19,7 +19,7 @@ use std::{
collections::{HashMap, HashSet, VecDeque},
io,
net::{IpAddr, SocketAddr},
sync::{Arc, Mutex, RwLock},
sync::{Arc, RwLock},
};
use thiserror::Error;
use tokio::{process::Command, signal, sync::Notify};
@@ -70,7 +70,6 @@ pub struct Service {
/// map from capture handle to connection info
incoming_conn_info: HashMap<ClientHandle, Incoming>,
next_trigger_handle: u64,
window_identifier: Arc<Mutex<Option<input_capture::WindowIdentifier>>>,
}
#[derive(Debug)]
@@ -102,13 +101,7 @@ impl Service {
// input capture + emulation
let capture_backend = config.capture_backend().map(|b| b.into());
let window_identifier = Arc::new(Mutex::new(None));
let capture = Capture::new(
capture_backend,
conn,
config.release_bind(),
window_identifier.clone(),
);
let capture = Capture::new(capture_backend, conn, config.release_bind());
let emulation_backend = config.emulation_backend().map(|b| b.into());
let emulation = Emulation::new(emulation_backend, listener);
@@ -133,7 +126,6 @@ impl Service {
incoming_conn_info: Default::default(),
incoming_conns: Default::default(),
next_trigger_handle: 0,
window_identifier,
};
Ok(service)
}
@@ -226,20 +218,6 @@ impl Service {
self.update_enter_hook(handle, enter_hook)
}
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)
}
});
}
}
}