feat: opt-in keyboard hotplug via keyboard_device_names allowlist#165
Merged
goodroot merged 4 commits intogoodroot:mainfrom Apr 19, 2026
Merged
feat: opt-in keyboard hotplug via keyboard_device_names allowlist#165goodroot merged 4 commits intogoodroot:mainfrom
goodroot merged 4 commits intogoodroot:mainfrom
Conversation
some input devices report enough EV_KEY codes to pass the shortcut
capability filter in _discover_keyboards without being keyboards —
notably logitech multi-device mice (M720, MX series) that advertise
super, alt, and common letter keys for software-macro buttons.
auto-discover then grabs them, which breaks the mouse.
add optional `keyboard_device_names` config: a list of device names
that _discover_keyboards is allowed to grab. when set, only listed
devices are considered; mice/media controllers get skipped. when
unset (default), behavior is unchanged.
priority, consistent with existing single-device overrides:
selected_device_name > selected_device_path > keyboard_device_names
> auto-discover
threaded through all 9 GlobalShortcuts() call sites in main.py.
schema enforces array-of-strings or null.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
keyboards plugged in after the service starts (e.g. docking a laptop
to a usb-c monitor, bluetooth keyboard reconnecting) were invisible
until the service was restarted, because _discover_keyboards() runs
once at startup and the event loop only reads from already-attached
devices.
add pyudev MonitorObserver on subsystem=input. mirrors the existing
device_monitor.py pattern (subsystem=sound for microphone hotplug)
with a new keyboard_monitor.py. on 'add' events for /dev/input/event*
nodes, open the device, apply the same filters as initial discovery,
and attach it to the live event loop. on 'remove' events, drop it
promptly so the event loop doesn't have to wait for the next read
error.
opt-in via the allowlist added in the previous commit:
_start_hotplug_monitor returns early when keyboard_device_names is
nil. rationale: the same aggressive discovery that makes hotplug
useful (grab anything that passes the target-key capability filter)
also grabs mice/media controllers on dock. the allowlist is the
user's consent that they've listed real keyboards — without it we
stay on the legacy startup-only path.
self-grab prevention: skip any device whose name contains "hyprwhspr"
in both _discover_keyboards and the hotplug path. grabbing our own
UInput virtual keyboard creates a feedback loop (physical key ->
virtual -> re-grab) that locks out all input. relevant on in-process
restart when a stale virtual-keyboard node may briefly be present.
concurrency:
- observer runs on its own daemon thread; each event spawns a
short-lived worker so a slow grab can't block the next event.
- self._device_lock already guards self.devices / self.device_fds;
every add/remove site acquires it, with a double-check-under-lock
to handle racey duplicate adds.
- stop() tears down monitor first, then joins listener, then
closes devices — reverse order of construction.
graceful degradation: pyudev missing or observer start failure logs
one line and returns. service works identically to upstream for
already-connected keyboards.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
makes the allowlist self-documenting. previously a user who saw a
mouse get grabbed had no obvious path to a fix — they had to read
source.
changes to `hyprwhspr keyboard list`:
- `[ALLOWED]` marker on devices on the allowlist; `[VIRTUAL]` on
hyprwhspr's UInput device, ydotoold, and similar virtual nodes
(so users don't accidentally add them to the allowlist);
alongside the existing `[SELECTED]`.
- when an allowlist is set, print it back and note that hotplug is
enabled for listed devices. warn about entries that don't match
any present device (catches typos vs. just-unplugged).
- when no selection is set, suggest the allowlist with a ready-to-
paste snippet using a real device name from the user's system.
also explains that setting it enables hotplug.
drop the grab()/ungrab() accessibility test in
get_available_keyboards(). that test failed on devices the running
service already grabbed, so `hyprwhspr keyboard list` hid the
primary keyboard from its own output. a successful InputDevice()
open is sufficient to list the device.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Owner
|
Oh dang, this is really interesting. I'll give this a review shortly. Thank you very much for taking the time to put up the pull request. |
Owner
|
Thanks again. I will bump the AUR version for the main package in the next day or so after I sprint things locally to be sure there's no regressions. This is just tradition. For now, it's available within the hyprwhspr-git AUR package and will also in the manual install method for everyone else. |
Contributor
Author
|
Thanks very much for the quick review @goodroot . Very much appreciate your work on this package; cheers! |
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.
adds pyudev-based keyboard hotplug detection. mirrors the existing
device_monitor.pypattern (which already watchessubsystem='sound'for microphone hotplug) with a newkeyboard_monitor.pythat watchessubsystem='input'.the problem:
_discover_keyboards()runs once at startup, so a keyboard plugged in later (docking a laptop, bluetooth reconnecting) is invisible until the service restarts. surfaced for me on a usb-c dock with an external keyboard.opt-in design: a non-nil
keyboard_device_namesallowlist is the single switch.why gate on the allowlist: some input devices pass the
target_keys.issubset(available_keys)capability filter without being keyboards. E.g. the logitech m720 triathlon mouse advertises 162EV_KEYcodes includingsuper,alt, and common letters. without the allowlist, aggressive hotplug grabs these and breaks mouse operation after a dock cycle. making the allowlist the opt-in switch couples the two correctly: by listing real keyboards, the user has consented to runtime discovery of exactly those devices.config:
{ "keyboard_device_names": [ "AT Translated Set 2 keyboard", "SONiX USB Keyboard" ] }self-grab prevention: both discovery paths skip any device whose name contains
"hyprwhspr". grabbing our ownUInputvirtual keyboard creates a feedback loop (physical → virtual → re-grab) that locks out all input. relevant on in-process restart when a stale virtual-keyboard node may linger briefly.cli improvements to
hyprwhspr keyboard list:[ALLOWED]/[VIRTUAL]markers alongside[SELECTED]grab()/ungrab()accessibility test inget_available_keyboards(); it failed on devices the running service already grabbed, hiding the primary keyboard from its own cligraceful degradation: pyudev missing → observer doesn't start, allowlist still filters startup discovery. priority between single-device and allowlist config:
selected_device_name>selected_device_path>keyboard_device_names> auto-discover. consistent between startup and hotplug paths.commits (each self-contained and bisect-safe):
feat: add keyboard_device_names allowlist— config, schema, main.py threading, startup filterfeat: auto-detect hotplugged keyboards via pyudev— monitor, hotplug path, self-grab filter, start/stop wiring, gated on the allowlist from hypewhspr : No microphone available #1feat(cli): show allowlist status in keyboard list— markers, guidance, drop stale test-grabtested locally on an arch laptop across a dock cycle: external keyboard attaches in ~1s of plug-in, triathlon mouse correctly filtered, no lockups observed.