-
Notifications
You must be signed in to change notification settings - Fork 0
hotkeys
Crate: crates/voxctrl-hotkeys/
VoxCtrl listens for global keyboard shortcuts that work regardless of which application has focus. The implementation is platform-specific: evdev on Linux and Win32 hooks on Windows.
Uses /dev/input/event* devices directly, bypassing the desktop environment entirely. This means hotkeys work in:
- X11 sessions
- Wayland sessions
- TTY / no-DE environments
The evdev keyboard device can be specified via audio.evdev_device in config (e.g. "/dev/input/event4"). If not set, the listener discovers keyboard devices automatically.
Requirement: The user must be in the input group (or have read access to /dev/input/event*):
sudo usermod -aG input $USER
# Log out and back inUses Win32 SetWindowsHookEx with WH_KEYBOARD_LL to intercept global keyboard events. No special permissions are required.
Each binding specifies a gesture that controls when recording starts and stops.
Key Down ──► START RECORDING (GestureKind::Start)
Key Up ──► STOP RECORDING (GestureKind::Stop)
Most natural for short dictations. Recording is exactly as long as the key is held.
The hold_threshold_ms field (default 200ms) sets the minimum hold duration before a recording start is registered, preventing accidental triggers.
Key Down (1st press) ──► START RECORDING
Key Down (2nd press) ──► STOP RECORDING
For longer dictations where holding a key would be tiring. Press once to start, press again to stop.
Rapid press+release twice within tap_ms ──► START RECORDING
(then behaves as toggle for stop)
Two rapid presses trigger recording. The tap_ms field (default 250ms) sets the inter-press window. Distinguishes from accidental single presses.
Double-tap within tap_ms, and keep the key held down on the second tap ──► START RECORDING
Release the key ──► STOP RECORDING
Triggers recording when the key is double-tapped and held. Recording continues as long as the key remains held, and stops when released. Enforces hold_threshold_ms on the second press to distinguish from a standard double-tap, and has a 2-minute safety timeout.
Base Keys held down ──► Wait for Subkey press
Subkey pressed ──► START RECORDING (GestureKind::Start)
Any Base Key released ──► STOP RECORDING (GestureKind::Stop)
A split configuration where you configure:
-
Base Combo: One or more keys that must be held down simultaneously (e.g.
KEY_LEFTCTRLandKEY_LEFTALT). -
Subkey Trigger: A single key pressed once base keys are held (e.g.
KEY_SPACE).
Recording starts immediately when the subkey trigger is pressed while the base keys are held. Recording continues as long as you keep holding the base keys, and stops immediately when you release any of the base keys. Releasing the subkey itself does not stop recording.
This is highly useful for advanced shortcut patterns without causing desktop keyboard layout or application conflicts.
Keys use evdev event code names on Linux. On Windows, the same names are mapped to Virtual Key codes internally.
| Name | Key |
|---|---|
KEY_LEFTCTRL |
Left Ctrl |
KEY_RIGHTCTRL |
Right Ctrl |
KEY_LEFTSHIFT |
Left Shift |
KEY_RIGHTSHIFT |
Right Shift |
KEY_LEFTALT |
Left Alt |
KEY_RIGHTALT |
Right Alt / AltGr |
KEY_LEFTMETA |
Left Super / Windows key |
KEY_RIGHTMETA |
Right Super / Windows key |
KEY_CAPSLOCK |
Caps Lock |
| Name | Key |
|---|---|
KEY_SPACE |
Space |
KEY_ENTER |
Enter |
KEY_TAB |
Tab |
KEY_ESCAPE |
Escape |
KEY_BACKSPACE |
Backspace |
KEY_F1–KEY_F12
|
Function keys |
KEY_A–KEY_Z
|
Letter keys |
KEY_0–KEY_9
|
Number row |
To discover the evdev name for any key:
# Install evtest if not present
sudo apt install evtest
# Run (pick your keyboard device)
sudo evtest /dev/input/event2
# Press the key — look for "EV_KEY" lines:
# Event: type 1 (EV_KEY), code 125 (KEY_LEFTMETA), value 1Hotkey bindings update at runtime without restarting the listener. When bindings are saved via the UI or save_bindings IPC command:
- New bindings are written to
bindings.toml - Sent through the
hotkey_reloadercrossbeam channel - The listener thread receives them and swaps its binding state table
keys is an array. All keys must be pressed for the gesture to activate. Order doesn't matter:
keys = ["KEY_LEFTMETA", "KEY_SPACE"]
# Fires when both Left-Super AND Space are held, in any orderThe output of the hotkey system is a stream of GestureEvent values sent on the gesture_tx channel:
pub struct GestureEvent {
pub binding_id: String,
pub binding_label: String,
pub target_id: String, // comma-joined for multi-target
pub kind: GestureKind,
}
pub enum GestureKind {
Start, // Begin recording
Stop, // End recording
}lib.rs receives these and coordinates the audio recorder and inference pipeline.
If two bindings share the same key combo, the first matching binding wins (the one listed first in bindings.toml). Disable unused bindings rather than leaving conflicts active:
[[binding]]
id = "old_binding"
disabled = true
keys = ["KEY_LEFTMETA", "KEY_SPACE"]
# ...