Skip to content

feat: add Linux support (evdev + HID++ + uinput)#33

Merged
thisislvca merged 17 commits intoTomBadash:masterfrom
hieshima:feat/linux-support
Mar 23, 2026
Merged

feat: add Linux support (evdev + HID++ + uinput)#33
thisislvca merged 17 commits intoTomBadash:masterfrom
hieshima:feat/linux-support

Conversation

@hieshima
Copy link
Copy Markdown
Contributor

Add Linux platform support for Mouser, including mouse event interception, keyboard action simulation, HID++ device communication, and per-app profile detection.

Linux implementation:

  • mouse_hook.py: evdev grab + uinput forwarding for button/scroll blocking, gesture tracking with threading lock, crash guard (atexit + signal handlers) to release grab on abnormal exit
  • key_simulator.py: evdev UInput virtual keyboard, 22 actions with thread-safe lazy initialization
  • app_detector.py: xdotool + /proc for foreground app detection (X11)
  • config.py: XDG-compliant config path (~/.config/Mouser)
  • requirements.txt: evdev>=1.6 Linux dependency

Cross-platform fixes:

  • hid_gesture.py: support both "pip install hid" and "pip install hidapi" via _HidDeviceCompat wrapper; cap HID++ control discovery at 32 with circuit breaker to prevent DoS from malicious devices
  • config.py: atomic config writes (tempfile + os.replace) to prevent data loss on crash; restrictive file permissions (0o600 on POSIX); type validation on config load

Note: Cross-platform changes (config.py, hid_gesture.py) were reviewed against Windows/macOS code paths but only runtime-tested on Linux (Nobara/Fedora, Bluetooth MX Master 3S).

Tested: 27/27 unit tests pass. Manual verification on Nobara Linux with Logitech MX Master 3S over Bluetooth.

hieshima and others added 5 commits March 19, 2026 13:22
Add full Linux platform support for Mouser, including mouse event
interception, keyboard action simulation, HID++ device communication,
and per-app profile detection.

Linux implementation:
- mouse_hook.py: evdev grab + uinput forwarding for button/scroll
  blocking, gesture tracking with threading lock, crash guard
  (atexit + signal handlers) to release grab on abnormal exit
- key_simulator.py: evdev UInput virtual keyboard, 22 actions
  with thread-safe lazy initialization
- app_detector.py: xdotool + /proc for foreground app detection (X11)
- config.py: XDG-compliant config path (~/.config/Mouser)
- requirements.txt: evdev>=1.6 Linux dependency

Cross-platform fixes:
- hid_gesture.py: support both "pip install hid" and "pip install
  hidapi" via _HidDeviceCompat wrapper; cap HID++ control discovery
  at 32 with circuit breaker to prevent DoS from malicious devices
- config.py: atomic config writes (tempfile + os.replace) to prevent
  data loss on crash; restrictive file permissions (0o600 on POSIX);
  type validation on config load

Note: Cross-platform changes (config.py, hid_gesture.py) were
reviewed against Windows/macOS code paths but only runtime-tested
on Linux (Nobara/Fedora, Bluetooth MX Master 3S).

Tested: 27/27 unit tests pass. Manual verification on Nobara Linux
with Logitech MX Master 3S over Bluetooth.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@thisislvca
Copy link
Copy Markdown
Collaborator

Hey, thanks so much for contributing!

I pushed a few follow-up commits directly onto this PR to add some missing features:

  • added Linux app profile creation/discovery
  • added regression coverage for the Linux app-profile path
  • enabled gesture swipe mappings in the Linux UI
  • documented current Linux runtime requirements and caveats

Before we merge, could you please test this on Linux and confirm:

  • app profile creation works
  • per-app auto-switching works
  • gesture tap + swipe mappings work
  • button remapping works with your current permissions setup

Thanks again 🙏

@hieshima
Copy link
Copy Markdown
Contributor Author

Thanks for adding these features! I tested everything on Linux (Nobara 43, Wayland, Logitech MX
Master 3S via Bluetooth).

Test Results:

Feature Status Notes
App profile creation Pass Successfully created a Firefox profile
Per-app auto-switching Fail See details below
Gesture tap + swipe mappings Pass Both tap and directional swipes work correctly
Button remapping Pass Was already working with current permissions setup

Per-app auto-switching issue:

The profile for Firefox is saved with "/usr/bin/firefox" in the apps list, but the actual
process detected at runtime via /proc/PID/exe resolves to /usr/lib64/firefox/firefox. Since
/usr/bin/firefox is a small launcher script (not a symlink), the paths never match and the
profile switch silently fails — it stays on "default".

This is not Nobara-specific. On most Linux distros, many apps use wrapper scripts in /usr/bin/
while the real binary lives elsewhere (e.g., /usr/lib/ or /usr/lib64/). Firefox, Chromium, VS
Code, etc. all follow this pattern.

Additional findings:

  • Changing the gesture threshold slider does not appear to trigger a save (the value resets on
    restart).

Heads up: I'll also be pushing a fix for a separate issue I found during long-running sessions
— the Bluetooth mouse disconnects when the screen turns off, and the evdev hook doesn't switch
back to it when it reconnects. Will be a separate commit.

@thisislvca
Copy link
Copy Markdown
Collaborator

Ok awesome, thanks! Will fix all of these tomorrow morning, feel free to add your commit for that fix thanks again 💪

@thisislvca
Copy link
Copy Markdown
Collaborator

Just pushed fixes for both of the issues you found:

  • fixed Linux app matching so wrapper-script paths like /usr/bin/firefox still resolve to the right app profile
  • fixed the gesture threshold slider so it actually persists after restart

If you get a chance, could you pull the latest on this PR and re-test:

  • per-app auto-switching
  • gesture threshold persistence after restart

The reconnect issue you mentioned can stay as a separate follow-up if that’s easier, happy to review that too once you push it. Thanks again

thisislvca and others added 4 commits March 22, 2026 12:28
When the Bluetooth mouse disconnects (e.g. screen-off power management),
the evdev hook falls back to another input device. Previously it stayed
stuck on that fallback even after the Logitech reconnected via HID++.

Now _on_hid_connect signals a rescan so the evdev loop switches back to
the Logitech device automatically.

Also suppress duplicate log lines:
- Battery level only printed when it changes
- "No compatible device" retry only printed once per disconnect cycle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously the engine read the mouse's current DPI on startup and
overwrote the saved config value, discarding the user's preference.
Now it applies the saved DPI to the mouse instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The existing app detector relied on xdotool, which only works on X11.
On KDE Wayland, native windows are invisible to xdotool and per-app
profile switching silently failed.

Add session-type detection with fallback:
- KDE Wayland: use kdotool (requires `dnf install kdotool`)
- X11: existing xdotool logic (unchanged)
- GNOME and other Wayland compositors: not yet supported (returns None)

Tested on Nobara 43 (KDE Plasma 6 + Wayland).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@hieshima
Copy link
Copy Markdown
Contributor Author

Thank you for addressing those two issues.

Re-test results

  • Gesture threshold persistence: Pass — value persists after restart.

  • Per-app auto-switching: Your path matching fix is correct — it properly resolves wrapper-script paths like /usr/bin/firefox to the actual binary via the app catalog. However, testing your fix revealed a deeper issue: per-app switching still doesn't work on Wayland desktops.

The root cause is in app_detector.py: get_foreground_exe() relies on xdotool, which is an X11 tool. On Wayland (now the default on most Linux distros), native Wayland windows are invisible to xdotool — it can only see legacy XWayland windows. So the path matching logic never gets a chance to run because the detector can't identify the foreground window in the first place.

To summarize the two layers:

  • Layer 1 — Window detection: Which app is in the foreground? (broken on Wayland)
  • Layer 2 — Path matching: Does the detected app match a profile? (your fix works)

My previous report only identified Layer 2. After your fix, testing revealed Layer 1 was the actual blocker.

Commits I'm pushing

  1. fix(linux): reconnect evdev after BT mouse resumes + reduce log noise When the BT mouse disconnects (e.g. screen turns off), the evdev hook now automatically switches back to the Logitech when it reconnects. Also suppresses duplicate battery and retry log lines.

  2. fix: persist DPI setting across restartsThe engine was reading the mouse's current DPI on startup and overwriting the saved config. Now it applies the saved DPI to the mouse instead.

  3. feat(linux): add KDE Wayland app detection for per-app profile switching Adds kdotool support for KDE Wayland (requires dnf install kdotool). Falls back to existing xdotool on X11. GNOME and other Wayland compositors are not yet supported — GNOME lacks a built-in API for this (Shell.Eval is disabled since GNOME 41, and the Introspect interface is restricted to privileged callers).

Other observations

  • DPI persistence seemed to be a pre-existing issue (didn't see in other commits, PRs & issues).
  • QML TypeError warnings appear on exit (cleanup ordering) — not blocking, just
    noisy.

All tests were done on Nobara 43 (KDE Plasma 6 + Wayland) with Logitech MX Master 3S via Bluetooth.

hieshima and others added 3 commits March 22, 2026 20:29
Add space_left and space_right actions to the Linux key_simulator,
matching the Windows and macOS implementations. Uses Ctrl+Super+Left/Right
which is standard across GNOME and KDE.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@thisislvca
Copy link
Copy Markdown
Collaborator

Just pushed two last follow-up fixes:

  • added an xdotool fallback if kdotool is unavailable on KDE Wayland
  • made Linux desktop switch actions use GNOME defaults on GNOME and KDE defaults on KDE

If you get a chance, could you do one last quick pass before we merge just to confirm:

  • per-app switching still works on your KDE Wayland setup
  • reconnect after screen-off / BT resume still works
  • desktop switching works as expected on your setup

If all good after that I think we should be good to merge, thanks again

@hieshima
Copy link
Copy Markdown
Contributor Author

Thanks for reviewing! All three are tested postive.

I also have a follow-up PR ready (feat/mode-shift-mapping) that adds mode shift button remapping, a wheel toggle (ratchet/free spin), and custom keyboard shortcut mapping. I'll open it once this PR is merged.

@thisislvca thisislvca merged commit 5963aef into TomBadash:master Mar 23, 2026
1 check passed
@thisislvca
Copy link
Copy Markdown
Collaborator

Appreciate it, just fixed a failing test and merged! Looking forward to reviewing your next PR too :)

@farfromrefug
Copy link
Copy Markdown

farfromrefug commented Mar 25, 2026

@hieshima i am trying this on ubuntu.
in the console i see:

[MouseHook] Found mouse: Logitech MX Anywhere 3 (/dev/input/event22) vendor=0x046D
[MouseHook] Grabbed Logitech MX Anywhere 3 (/dev/input/event22)

But the mouse does not appear in the UI and is not "handled". Any idea why?

EDIT: it seems it is because evdev is picking up the mouse but no _on_hid_connect is triggered.
Also i see that if i start mouser while my mouse is connected it grabs the name of the mouse. If i disconnect/connect while mouser is running it is finding a different evdev/name:

[Mouser] Engine started — remapping is active
[MouseHook] Found mouse: Logitech MX Anywhere 3 (/dev/input/event22) vendor=0x046D
[MouseHook] Grabbed Logitech MX Anywhere 3 (/dev/input/event22)
[MouseHook] Device Connected
[MouseHook] Device disconnected: [Errno 19] No such device
[MouseHook] evdev device released
[HidGesture] Battery read timed out
[MouseHook] Found mouse: FTCS1000:00 2808:0101 Mouse (/dev/input/event6) vendor=0x2808
[MouseHook] Grabbed FTCS1000:00 2808:0101 Mouse (/dev/input/event6)
[HidGesture] DPI set timed out

@hieshima
Copy link
Copy Markdown
Contributor Author

@farfromrefug Thanks again for reporting this.

I put together a follow-up in PR #55 that should address the two Linux symptoms you described:

  • the mouse should now appear in the UI as soon as evdev grabs it, without waiting for HID++
  • Linux evdev rediscovery should now ignore non-Logitech candidates, so it should no longer grab the FTCS1000 device on reconnect

If HID++ still does not come up on your Ubuntu setup, Mouser now logs HID connection failures more explicitly as well (for example, when a HID candidate opens but feature discovery fails, or when no Logitech HID interface is found yet). If you’re willing to retest, that would be very helpful.

@farfromrefug
Copy link
Copy Markdown

@hieshima awesome. I tested your PR and it seems much better. The only issue i have is that the DPI slider is not visible in the settings. Normal?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants