Compare commits

...

2 Commits

Author SHA1 Message Date
onelock
00c5eee8d8 nix(chore): set nixpkgs to nixos-26.05, update flake.lock (#495)
fixes #492
Co-authored-by: KristijanZic <kristijan.zic@10x.engineering>
2026-09-07 09:15:09 +02:00
mrcha033
6b1eddef7a fix(libei): only restart the capture session on device change under GNOME
`EiEvent::DeviceRemoved` currently tears down and recreates the whole
input-capture session. That is a workaround for mutter, which stops
delivering events after a device change, but it is applied on every
compositor.

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

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

The default is derived from XDG_CURRENT_DESKTOP and can be overridden
with LM_RESTART_SESSION_ON_DEVICE_CHANGE=1/0 for compositors that need
the mutter behaviour but do not advertise themselves as GNOME.
2026-08-31 14:11:10 +02:00
5 changed files with 67 additions and 18 deletions

14
flake.lock generated
View File

@@ -2,16 +2,16 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1772963539,
"narHash": "sha256-9jVDGZnvCckTGdYT53d/EfznygLskyLQXYwJLKMPsZs=",
"lastModified": 1788405554,
"narHash": "sha256-r2f1oUwixlgq9zOdYLqJLfS/lWBT60/IITjhTKI59JU=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "9dcb002ca1690658be4a04645215baea8b95f31d",
"rev": "a5cc6f2c37bf518436dc8d1c288ccd0c43c2f4c4",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"ref": "nixos-26.05",
"repo": "nixpkgs",
"type": "github"
}
@@ -29,11 +29,11 @@
]
},
"locked": {
"lastModified": 1773025773,
"narHash": "sha256-Wik8+xApNfldpUFjPmJkPdg0RrvUPSWGIZis+A/0N1w=",
"lastModified": 1788505699,
"narHash": "sha256-PSPDCdbEBEbSDCWcqv5GkbyD2ACQ9Diz6vmEbWdy4dM=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "3c06fdbbd36ff60386a1e590ee0cd52dcd1892bf",
"rev": "9eccf73c5b810052f08aa77ae0548c383259f17f",
"type": "github"
},
"original": {

View File

@@ -1,7 +1,8 @@
{
description = "Nix Flake for lan-mouse";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
# 26.05 used here to keep allowing builds on x86_64-darwin, bump this to unstable once they deprecate 26.05
nixpkgs.url = "github:nixos/nixpkgs/nixos-26.05";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
@@ -30,7 +31,24 @@
let
pkgs = import nixpkgs {
inherit system;
overlays = [ rust-overlay.overlays.default ];
overlays = [
rust-overlay.overlays.default
#NOTE: appstream's meson build injects the literal string
# "none required" into linker flags on 'aarch64-darwin', causing clang to fail.
# remove this overlay once we bump nixpkgs to unstable branch
(
final: prev:
lib.optionalAttrs prev.stdenv.hostPlatform.isDarwin {
appstream = prev.appstream.overrideAttrs (old: {
postConfigure = (old.postConfigure or "") + ''
substituteInPlace build.ninja \
--replace-fail "none required" ""
'';
});
}
)
];
};
# Default toolchain for devshell
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
@@ -80,7 +98,7 @@
libadwaita
librsvg
]
++ lib.optionals pkgs.stdenv.isLinux [
++ lib.optionals pkgs.stdenv.hostPlatform.isLinux [
libX11
libXtst
];

View File

@@ -18,12 +18,12 @@ use reis::{
use std::{
cell::Cell,
collections::HashMap,
io,
env, io,
num::NonZeroU32,
os::unix::net::UnixStream,
pin::Pin,
rc::Rc,
sync::Arc,
sync::{Arc, LazyLock},
task::{Context, Poll},
};
use tokio::{
@@ -50,6 +50,25 @@ use super::{
* prevents receiving further events after a session has been disabled once.
* Therefore the session needs to be recreated when the barriers are updated */
/* mutter also kills the session whenever ei devices come and go, so there the
* whole session has to be torn down and recreated on every device change.
* Elsewhere that is pure overhead: each restart costs a CreateSession +
* ConnectToEIS round trip, and compositors that keep per-session state around
* (hyprland leaks a keymap fd per eis session, see hyprwm/Hyprland) can be
* driven out of file descriptors by the churn.
* Set LM_RESTART_SESSION_ON_DEVICE_CHANGE=1/0 to override the default. */
static RESTART_SESSION_ON_DEVICE_CHANGE: LazyLock<bool> =
LazyLock::new(restart_session_on_device_change);
fn restart_session_on_device_change() -> bool {
match env::var("LM_RESTART_SESSION_ON_DEVICE_CHANGE").as_deref() {
Ok("1") => true,
Ok("0") => false,
_ => env::var("XDG_CURRENT_DESKTOP")
.is_ok_and(|desktops| desktops.to_uppercase().split(':').any(|d| d == "GNOME")),
}
}
/// events that necessitate restarting the capture session
#[derive(Clone, Copy, Debug)]
enum LibeiNotifyEvent {
@@ -555,20 +574,32 @@ async fn handle_ei_event(
s.seat.bind_capabilities(all_capabilities);
context.flush().map_err(|e| io::Error::new(e.kind(), e))?;
}
EiEvent::SeatRemoved(_) | /* EiEvent::DeviceAdded(_) | */ EiEvent::DeviceRemoved(_) => {
EiEvent::SeatRemoved(_) => {
log::debug!("releasing session: {ei_event:?}");
release_session.notify_waiters();
}
/* EiEvent::DeviceAdded(_) | */
EiEvent::DeviceRemoved(_) => {
if *RESTART_SESSION_ON_DEVICE_CHANGE {
log::debug!("releasing session: {ei_event:?}");
release_session.notify_waiters();
} else {
log::debug!("ignoring device change: {ei_event:?}");
}
}
EiEvent::DevicePaused(_) | EiEvent::DeviceResumed(_) => {}
EiEvent::DeviceStartEmulating(_) => log::debug!("START EMULATING"),
EiEvent::DeviceStopEmulating(_) => log::debug!("STOP EMULATING"),
EiEvent::Disconnected(d) => {
return Err(CaptureError::Disconnected(format!("{:?}", d.reason)))
return Err(CaptureError::Disconnected(format!("{:?}", d.reason)));
}
_ => {
if let Some(pos) = current_client {
for event in Event::from_ei_event(ei_event) {
event_tx.send((pos, CaptureEvent::Input(event))).await.expect("no channel");
event_tx
.send((pos, CaptureEvent::Input(event)))
.await
.expect("no channel");
}
}
}

View File

@@ -31,7 +31,7 @@ rustPlatform.buildRustPackage {
libadwaita
librsvg
]
++ lib.optionals stdenv.isLinux [
++ lib.optionals stdenv.hostPlatform.isLinux [
libX11
libXtst
];

View File

@@ -23,12 +23,12 @@ in {
};
systemd = mkOption {
type = types.bool;
default = pkgs.stdenv.isLinux;
default = pkgs.stdenv.hostPlatform.isLinux;
description = "Whether to enable to systemd service for lan-mouse on linux.";
};
launchd = mkOption {
type = types.bool;
default = pkgs.stdenv.isDarwin;
default = pkgs.stdenv.hostPlatform.isDarwin;
description = "Whether to enable to launchd service for lan-mouse on macOS.";
};
settings = lib.mkOption {