From 3c607d9babd2a1c3b5330bef749af53d8d13ed53 Mon Sep 17 00:00:00 2001 From: SusieeTheFemboy <96988330+SusieeTheLinuxUser@users.noreply.github.com> Date: Thu, 29 Oct 2026 05:18:49 +0100 Subject: [PATCH 1/3] Mouse: keep looking while it's missing The mouse worker only reloaded from ratbagd at startup, after a command, or when HID++ reads failed. If that load failed (ratbagd not running yet, mouse unplugged, receiver asleep), nothing set reload again, so the Mouse page stayed on "No mouse found" until a restart, even after following its own "start ratbagd" hint. Retry every 5 seconds while it's missing, like the other workers poll. Co-Authored-By: Claude Opus 5.5 --- src/workers.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/workers.rs b/src/workers.rs index bae93bc..a89c8af 100644 --- a/src/workers.rs +++ b/src/workers.rs @@ -293,10 +293,13 @@ fn mouse(st: Shared, ctx: Context, rx: Receiver) { let mut last: Option<(u16, u8)> = None; // (profile, stage) as the mouse reports it let mut popup: Option = None; let mut reload = true; + // while missing, look again now and then: ratbagd started later, mouse plugged in or back from an unplug + let mut retry_at: Option = None; loop { - if reload { + if reload || retry_at.is_some_and(|t| Instant::now() >= t) { reload = false; + retry_at = None; match rb.load() { Ok(m) => { if hidpp.is_none() { @@ -304,7 +307,10 @@ fn mouse(st: Shared, ctx: Context, rx: Receiver) { } update(&st, &ctx, |s| s.mouse = Dev::Ok(m)); } - Err(e) => missing(format!("{e:#}")), + Err(e) => { + missing(format!("{e:#}")); + retry_at = Some(Instant::now() + Duration::from_secs(5)); + } } } From ec2ff64bd74d675c65f9d0208d65d2cd0eba47a9 Mon Sep 17 00:00:00 2001 From: SusieeTheFemboy <96988330+SusieeTheLinuxUser@users.noreply.github.com> Date: Thu, 29 Oct 2026 05:23:54 +0100 Subject: [PATCH 2/3] Mouse: refresh the HID list before opening HID++; drop a stale DPI stage Review follow-ups: the HidApi device list is a snapshot from startup, so a Logitech mouse plugged in later loaded from ratbagd but never got live DPI. And when the mouse goes missing, the last mouse's dpi_stage stayed and could show for the next one. Co-Authored-By: Claude Opus 5.5 --- src/workers.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/workers.rs b/src/workers.rs index a89c8af..db8e4b8 100644 --- a/src/workers.rs +++ b/src/workers.rs @@ -280,7 +280,10 @@ fn cooler(st: Shared, ctx: Context, rx: Receiver) { fn mouse(st: Shared, ctx: Context, rx: Receiver) { use crate::mouse::{HidppDpi, Ratbag}; - let missing = |msg: String| update(&st, &ctx, |s| s.mouse = Dev::Missing(msg)); + let missing = |msg: String| update(&st, &ctx, |s| { + s.mouse = Dev::Missing(msg); + s.dpi_stage = None; // the next mouse may not report one + }); let rb = loop { match Ratbag::connect() { Ok(rb) => break rb, @@ -288,7 +291,7 @@ fn mouse(st: Shared, ctx: Context, rx: Receiver) { } std::thread::sleep(Duration::from_secs(10)); }; - let api = hidapi::HidApi::new().ok(); + let mut api = hidapi::HidApi::new().ok(); let mut hidpp: Option = None; let mut last: Option<(u16, u8)> = None; // (profile, stage) as the mouse reports it let mut popup: Option = None; @@ -303,12 +306,17 @@ fn mouse(st: Shared, ctx: Context, rx: Receiver) { match rb.load() { Ok(m) => { if hidpp.is_none() { - hidpp = api.as_ref().and_then(|api| HidppDpi::open(api, &m.model).ok()); + // the device list is a snapshot: a mouse plugged in since isn't on it + hidpp = api.as_mut().and_then(|api| { + let _ = api.refresh_devices(); + HidppDpi::open(api, &m.model).ok() + }); } update(&st, &ctx, |s| s.mouse = Dev::Ok(m)); } Err(e) => { missing(format!("{e:#}")); + last = None; retry_at = Some(Instant::now() + Duration::from_secs(5)); } } From 23dfc2f918fe58e948d3f3a48dc60ec9cc4b8af6 Mon Sep 17 00:00:00 2001 From: SusieeTheFemboy <96988330+SusieeTheLinuxUser@users.noreply.github.com> Date: Thu, 29 Oct 2026 05:30:39 +0100 Subject: [PATCH 3/3] Mouse: recheck every 30 s while there's no HID++; drop HID++ on a failed load Review follow-ups: without a HID++ handle (non-Logitech mouse, or opening it failed while the mouse slept) nothing noticed an unplug or retried the open, so the page kept a mouse that was gone. A failed load also kept the old HID++ handle, so a different mouse picked by ratbagd later would read the old one. Co-Authored-By: Claude Opus 5.5 --- src/workers.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/workers.rs b/src/workers.rs index db8e4b8..0a9782a 100644 --- a/src/workers.rs +++ b/src/workers.rs @@ -296,7 +296,8 @@ fn mouse(st: Shared, ctx: Context, rx: Receiver) { let mut last: Option<(u16, u8)> = None; // (profile, stage) as the mouse reports it let mut popup: Option = None; let mut reload = true; - // while missing, look again now and then: ratbagd started later, mouse plugged in or back from an unplug + // look again now and then while missing (ratbagd started later, mouse plugged in or back from an unplug) + // or without HID++, whose failing reads are otherwise the only sign of an unplug let mut retry_at: Option = None; loop { @@ -312,10 +313,14 @@ fn mouse(st: Shared, ctx: Context, rx: Receiver) { HidppDpi::open(api, &m.model).ok() }); } + if hidpp.is_none() { + retry_at = Some(Instant::now() + Duration::from_secs(30)); + } update(&st, &ctx, |s| s.mouse = Dev::Ok(m)); } Err(e) => { missing(format!("{e:#}")); + hidpp = None; // ratbagd may pick a different mouse next time last = None; retry_at = Some(Instant::now() + Duration::from_secs(5)); }