feat: custom keyboard/mouse hotkeys via low-level hooks - #13
Merged
Merged
Conversation
DavidHruby1
requested changes
Jun 4, 2026
DavidHruby1
left a comment
Owner
There was a problem hiding this comment.
Findings
- P1:
HotkeyListener.stop()can miss shutdown and leak active hooks/threads.
src/hotkey.py:87-98,src/hotkey.py:224-238
start()returns before the hook thread has definitely created a message queue and installed hooks.stop()only postsWM_QUITif_thread_idis already set, ignoresPostThreadMessageWfailure, then sets_thread = Noneafter a timed join even if the thread is still alive. If stop happens during startup or before the queue accepts thread messages, the listener can remain blocked inGetMessageWwith hooks installed. This can happen on quick shutdown or settings changes. Needs a ready event/message-queue synchronization,PostThreadMessageWresult handling/retry, and should not clear_threadunless the thread actually exited. - P2: Mouse hotkey recording is not actually global within the dialog.
src/settings_dialog.py:675-712
HotkeyCaptureEdit.start_recording()callsgrabKeyboard()but never grabs mouse input.mousePressEvent()will only see side/middle button presses delivered to thatQLineEdit, so users likely must hover/click directly over the capture field despite the UI saying "press keys or a mouse button". UsegrabMouse()/releaseMouse()or an event filter on the dialog/application while recording, and cover it with a test.
Merge Readiness
Not ready to merge. CI is green and the overall design is reasonable, but the listener shutdown race is a merge blocker because it can leave duplicate/global hooks running.
Checks Observed
PR #13 is open, non-draft, mergeable, and CI passes: compile (ubuntu / py3.12), test (windows / py3.11), test (windows / py3.12).
KristianP26
force-pushed
the
feat/custom-hotkeys
branch
from
June 4, 2026 10:28
e45330c to
eb8c173
Compare
Contributor
Author
|
Thanks for the review — both findings addressed, and the branch is rebased onto P1 —
P2 — mouse capture not global in the dialog
CI is green (compile + Windows tests). |
DavidHruby1
approved these changes
Jun 4, 2026
DavidHruby1
left a comment
Owner
There was a problem hiding this comment.
Previous findings are addressed. CI is green and this is ready to merge.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds custom hotkeys: bind any keyboard key/combination or a mouse side/middle button as the push-to-talk trigger, via a press-to-record UI, alongside the existing presets.
The Win32
RegisterHotKeylistener is replaced with global low-level hooks (WH_KEYBOARD_LL+WH_MOUSE_LL), which unlocks arbitrary keys + mouse buttons, native press/release detection (noGetAsyncKeyStatepolling), and swallowing the matched trigger so it doesn't reach the app underneath.What changed
config.py— new pureHotkeyvalue object (modifiers + one key/mouse trigger), serialized to a single canonical string (ctrl+alt+key:0x20,ctrl+mouse:x1). Methods:to_canonical,to_label,validate,parse. The 7 legacy preset strings auto-migrate on load. Presets inHOTKEY_OPTIONSswitched to canonical values. Removed the obsoleteRegisterHotKeyartifacts (MOD_*,HotkeyBinding,HOTKEY_BINDINGS).hotkey.py—HotkeyListenerrewritten to install LL keyboard + mouse hooks. The matching core (_on_kb_event/_on_mouse_event) is pure and OS-independent, so it is unit-tested without Win32.settings_dialog.py—HotkeyCaptureEditpress-to-record widget (Qt key/mouse events; Esc cancels), presets combo + "Custom…" entry, live validation feedback.main.py— parses the stored canonical string into aHotkeywhen building the listener.utils.py—HOTKEY_INVALID/HOTKEY_HOOK_FAILEDerror codes.Safety rules (validation)
Tests
New:
test_hotkey_model.py,test_hotkey_listener.py,test_settings_hotkey.py. Updated:test_mappings.py,test_tray_menu.py. Full suite: 60 passing.