Capture global hotkeys on Windows through raw input

A message-only window registers the keyboard and mouse with
RIDEV_INPUTSINK, so every key and button arrives as WM_INPUT whether or
not the client is in front. Raw input only observes, where a
WH_KEYBOARD_LL hook sits in the path of the system input queue and can
swallow a keystroke; it also reports the side buttons and both edges of
every key, which push-to-talk needs.

Keyboard codes stay in each platform's own numbering — X keycodes on
X11, set-1 scan codes with the E0/E1 escape folded into the high byte on
Windows — since that is what the input API reports and the key-naming
call expects. Bindings are per-machine either way, as TeamSpeak's own
per-OS keydefs are. Mouse buttons are unified on the X numbering, so
"Mouse 4" means the same thing on both.

The RAWINPUT decoding lives in its own class so it can be tested off
Windows; the window and its pump have not been run on Windows yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 18:40:50 +00:00
parent edc8a0ca3a
commit 752676e863
8 changed files with 720 additions and 10 deletions

View File

@@ -94,9 +94,14 @@ frontend supplies its own UI and audio backend while reusing `core` unchanged.
actions. Actions this client does not implement are listed but greyed out.
- Push-to-talk is one of those hotkeys; the button in **Options → Voice Activation**
edits that binding.
- Capture uses **XInput2 raw events**, as TeamSpeak's own Linux client does: no
- On Linux, capture uses **XInput2 raw events**, as TeamSpeak's own client does: no
privileges needed and the keystroke is not swallowed. X11's RECORD extension is kept
as a fallback for servers without XInput2.
- On **Windows**, capture uses **raw input** (`RIDEV_INPUTSINK`) through a message-only
window: it observes rather than intercepts, so unlike a low-level keyboard hook it
cannot swallow a keystroke or stall the system input queue, and it reports the side
buttons and both edges of every key. Bindings are stored as scan codes, so they follow
the physical key rather than the layout.
- On Wayland the hooks run inside Xwayland, so they see every X11 application, and keys
aimed at native Wayland windows only where the compositor forwards them — under KWin
that is *System Settings → Window Management → Legacy X11 App Support*, which is why
@@ -206,6 +211,9 @@ desktop/ com.ts3client.audio.desktop + com.ts3client.hotkey.desktop
├── XInput2InputHook global key/button capture via XInput2 raw events
├── XRecordInputHook the same via X11's RECORD extension, as a fallback
├── EvdevInputHook /dev/input fallback for Wayland sessions
├── WindowsInputHook the same on Windows: raw input into a message-only window
├── RawInput RAWINPUT layout and decoding, split out to be testable
├── X11KeyNamer/WindowsKeyNamer layout-aware key labels per platform
└── DesktopInputHooks picks the backend that suits the session
swing/ com.ts3client
@@ -244,4 +252,10 @@ swing/ com.ts3client
whisper and push-to-whisper, recording, plugins, server groups, talk power, 3D
sound, hardware ("local") microphone mute and the channel-traversal variants
beyond "Switch to Channel".
- Global hotkeys cover Linux and Windows; **macOS has no backend**, so hotkeys there are
inert (a `CGEventTap`, which needs Accessibility permission, is the way in). The
Windows backend is written but has not been run on Windows — only its decoding is
covered by tests.
- When no backend can start, push-to-talk does not work at all, not even with the window
focused; a focus-scoped fallback hook would restore the pre-hotkey behaviour.
- No file transfer, avatars, or server/channel administration UI yet.