Skip to content

Add mouse-to-mouse button remapping (left, right, middle, back, forward)#65

Open
guilamu wants to merge 4 commits intoTomBadash:masterfrom
guilamu:master
Open

Add mouse-to-mouse button remapping (left, right, middle, back, forward)#65
guilamu wants to merge 4 commits intoTomBadash:masterfrom
guilamu:master

Conversation

@guilamu
Copy link
Copy Markdown

@guilamu guilamu commented Mar 30, 2026

Add mouse-to-mouse button remapping (left, right, middle, back, forward)

Add mouse-to-mouse button remapping (left, right, middle, back, forward)
@alphatownsman
Copy link
Copy Markdown

would be nice to add map to more mouse button e.g. 5/6/7

@guilamu
Copy link
Copy Markdown
Author

guilamu commented Mar 31, 2026

This causes some instability, I'm working on it. @TomBadash do not merge right now. @alphatownsman I'll work on it after I fix those instability issues.

@guilamu
Copy link
Copy Markdown
Author

guilamu commented Mar 31, 2026

I've just updated my fork and it's now working without causing any crash. (Mx Master 3, W11). If anyone can confirm thanks!

@guilamu
Copy link
Copy Markdown
Author

guilamu commented Mar 31, 2026

Okay, here's a recap of all the bug fixes/features added since yesterday:

Bug 1: Stuck simulated button on HID++ stall (crash)

Files: hid_gesture.py, engine.py

Symptom: The app effectively crashes — a diverted button (mode_shift) fires inject_mouse_down(middle_click), but the HID++ channel stalls and the UP event never arrives. The simulated middle-click stays permanently pressed, freezing the UI.

Root cause: When _rx() returns no data for several consecutive reads (device firmware stall / sleep mid-press), nobody synthesizes the missing UP event. The 5-second safety timer in Engine existed but was too slow.

Fix:

  1. _force_release_stale_holds() in hid_gesture.py — New method. After 3 consecutive empty _rx() reads (~3s), synthesizes UP callbacks for any gesture or extra-diverted buttons still in held=True state.
  2. Disconnect cleanup now fires UP callbacks — Previously, _main_loop cleanup just reset self._held = False and info["held"] = False silently. Now it calls _on_up() / info["on_up"]() before clearing, so the Engine receives the release and calls inject_mouse_up().
  3. Safety timer reduced from 5s → 2s in engine.py _make_mouse_down_handler() — The threading.Timer that auto-releases a stuck button now fires after 2 seconds instead of 5.

Bug 2: Lost button remapping after device sleep (~1 hour)

File: hid_gesture.py

Symptom: After ~1 hour idle, the mode_shift button stops producing middle-clicks. Pressing it does the hardware default (scroll-mode toggle) instead. The log shows endless Smart Shift read FAILED / request timeout lines but no reconnect.

Root cause: When the mouse goes to sleep / power-cycles its BLE/USB link, the HID file handle stays open. _rx() returns None instead of raising an exception, so _main_loop never breaks out of its inner loop. No reconnect happens, so CID 0x00C4 is never re-diverted — the button reverts to its hardware default permanently.

Fix:

  1. Consecutive request timeout counter in _request() — Every successful response resets _consecutive_request_timeouts to 0; every timeout increments it. Logged in the timeout message for diagnostics.
  2. Auto-reconnect after 3 consecutive timeouts — The main event loop checks _consecutive_request_timeouts >= 3 at the top of each iteration and raises IOError to trigger the full reconnect cycle (close → re-open → re-discover features → re-divert buttons).
  3. Pending operations cleared on disconnect_pending_dpi, _pending_smart_shift, _pending_battery are all set to None during cleanup. Previously only _pending_battery was cleared, leaving the polling threads hung on stale flags.
  4. read_smart_shift() / read_dpi() / read_battery() clear _pending_* on timeout — If the 3-second caller-side wait expires, the pending flag is now cleared so the listener thread doesn't try to process a stale request on the next loop iteration.

Bug 3: Mouse button lag under disk I/O load

File: mouse_hook.py

Symptom: Pressing the back button 4 times quickly while Explorer is doing file operations — nothing happens for ~1s, then all 4 clicks fire at once.

Root cause: The Win32 low-level hook callback (_low_level_handler_inner) was calling _dispatch() synchronously. Each dispatch runs Engine handlers that call print() (which writes to the log file via the rotating file handler). When the disk is busy (file copy/delete dialogs), the print() blocks, stalling the hook's message pump. Windows queues subsequent mouse events and delivers them all at once when the pump resumes. (The macOS implementation already had an async dispatch queue; Windows didn't.)

Fix:

  1. _dispatch_queue (Queue) added to __init__ — The hook callback now puts the MouseEvent on the queue and returns immediately (the block/passthrough decision is still made synchronously).
  2. _dispatch_worker() background thread — Drains the queue and calls _dispatch() off the hook thread. Started in start(), joined in stop().
  3. Applied to both Windows and macOS implementations for consistency.

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.

2 participants