INPUT & ROUTING / HOTKEYS

Hotkeys & gestures

voxctrl listens for global shortcuts that work regardless of which application has focus. On Linux this happens through the XDG desktop portal, so voxctrl does not read your keyboard — your desktop owns the key grab and tells voxctrl exactly one thing: that its own shortcut fired. On Windows, Win32 low-level keyboard hooks are used, which is the only mechanism the platform offers for this kind of shortcut.

This is a change from earlier voxctrl versions, which read /dev/input/event* directly and required a udev rule granting keyboard-wide access. See why this changed below.

How shortcuts reach voxctrl

Two backends are available on Linux. The portal is the default and is used whenever your desktop supports it; the evdev path only ever engages as a fallback, and only if your system already allows the process to read input devices — voxctrl never grants itself that access.

Portal (default) evdev fallback
Can see keystrokes in other apps No Yes, all of them
Needs a udev rule or input group No Yes
Needs any permission setup No Yes, and voxctrl will not do it for you
Who chooses the keys You, through your desktop You, in voxctrl
Works on Wayland Yes Yes
Works on X11 Yes Yes
Works with no desktop (bare TTY) No Yes

Settings → Hotkeys and the setup window both state which of these is in use, live. If it says your desktop is handling the shortcuts, voxctrl is not reading your keyboard.

Portal — org.freedesktop.portal.GlobalShortcuts

voxctrl declares its shortcuts as preferred triggers over D-Bus; your desktop decides what actually binds — it may confirm with you, and it may assign different keys — and from then on sends Activated/Deactivated events when the shortcut is pressed and released. No permissions are required: nothing to install, no group to join, no rule to write, no logout. Supported by KDE Plasma (5.27+), GNOME 48+, and Hyprland. Compositors without the interface — Sway and most other wlroots compositors as of writing — fall through to the evdev fallback below.

! KDE Plasma accepts voxctrl's shortcut registration but, due to a confirmed upstream bug (bugs.kde.org #483639), leaves them unticked in System Settings → Shortcuts until you check the box yourself. There is no API that reports whether this step is done, so voxctrl cannot complete it for you — when it detects the portal + KDE combination, the Hotkeys tab shows a standing notice with a one-click Open Shortcut Settings button that jumps straight to the right panel.

If the portal refuses the session, the app distinguishes a missing portal ("no shortcuts portal available") from a refusal ("your desktop has one but declined the request") and shows the exact D-Bus error under Portal reported: in the setup window.

Linux evdev fallback

Only used when the portal is unavailable, and only if your system already permits this process to read input devices. In this mode voxctrl reads /dev/input/event* directly, so every keystroke on the system passes through the process — but nothing is logged, stored, or transmitted. Key names live briefly in memory and never cross into the UI layer or any network call, and the Hotkeys tab and setup window say so in plain language rather than staying quiet about it. A specific keyboard can be pinned with audio.evdev_device (e.g. "/dev/input/event4"); synthetic devices (uinput, XTEST, anything named "virtual") are always skipped so voxctrl cannot react to keystrokes it injects itself.

Windows

Uses the Win32 SetWindowsHookEx / WH_KEYBOARD_LL low-level keyboard hook. No special permissions are required. Like the evdev fallback, this hook sees all keystrokes — that is the only mechanism Windows offers for this kind of shortcut.

Why this changed

Reading /dev/input/event* requires a udev rule tagging input devices with uaccess (or membership of the input group). voxctrl used to install one during setup:

SUBSYSTEM=="input", KERNEL=="event*", TAG+="uaccess"

That rule isn't narrow — it grants every process running as you the ability to read every keystroke on the system: a compromised npm postinstall script, any Electron app, any shell one-liner. systemd's own defaults deliberately grant uaccess on input devices to joysticks and nothing else, precisely to avoid this. A dictation app shouldn't be the reason your machine's security posture changes, and it certainly shouldn't do it silently in a first-run wizard, so:

Administrator rights are still requested for one thing on Linux: installing host packages such as wtype and xdotool, which type the transcription into your focused window. That step touches no permissions.

Default bindings

Gesture Keys Action
Hold-to-talk Super + Space Hold while speaking, release to deliver
Toggle-to-talk Ctrl + Super + Space Tap to start, tap again to stop
Double-tap Alt Double-tap and hold Alt to record

All configurable in Settings → Hotkeys or directly in ~/.config/voxctrl/bindings.toml. Each gesture can be disabled without deleting it.

Gesture modes

Hold

Most predictable. Press, speak, release. Mic state is unambiguous — visible in the overlay. The hold_threshold_ms field (default 200ms) sets the minimum hold duration before a recording start is registered, preventing accidental triggers.

Toggle

Hands-free. Tap once to start, tap again to stop. The RMS noise gate also stops recording on silence if configured.

Double-tap

Reuses an existing modifier without colliding with normal usage. Double-tapping Alt never fires when Alt is held as part of Alt+Tab. Default tap window (tap_ms): 250ms.

Double-tap-hold

Double-tap and keep the key held down on the second tap to start recording; release the key to stop. Enforces hold_threshold_ms on the second press to distinguish from a standard double-tap, and has a 2-minute safety timeout.

The chord gesture (hold base keys, press a subkey to start) has been removed — it couldn't be expressed as a system shortcut, since the portal has no concept of a partially-held combo. Existing chord bindings aren't broken: on load they're converted to hold using the keys already in keys, and the obsolete subkey field is dropped the next time bindings are saved. Nothing needs to be edited by hand.

What can be a shortcut

An accelerator is any number of modifiers plus exactly one regular key — Super+Space, Ctrl+Alt+D, and F5 are all fine. Two shapes are not accelerators, and no desktop can bind them: a lone modifier with nothing else (a bare double-tapped Super is the one that catches people out), and two regular keys together (A+B). The key recorder in Settings → Hotkeys refuses these while you're recording and explains why, rather than saving a binding that silently never fires.

Conflict detection

The Settings UI checks for collisions as you record new keys:

double_tap and double_tap_hold on the same keys are not a conflict — they're a supported pairing (a quick double-tap runs one, double-tap-and-hold runs the other), and on the portal backend they're registered as a single system shortcut so your desktop is never asked to bind the same keys twice.

Recording new bindings

Press the Bind button next to a key field. The button turns orange and shows "Recording…". Press your keys. The display updates live as keys are held. Release — binding captured. This capture happens inside voxctrl's own focused window using ordinary browser key events — it is not a global listener.

Configuration

The full schema for ~/.config/voxctrl/bindings.toml with one binding per gesture type:

[[binding]]
id        = "dictate_hold"
label     = "Dictate (Hold)"
keys      = ["KEY_LEFTMETA", "KEY_SPACE"]
gesture   = "hold"
target_ids = ["default"]
hold_threshold_ms = 200

[[binding]]
id        = "agent_command"
label     = "Agent (Double-tap)"
keys      = ["KEY_LEFTCTRL"]
gesture   = "double_tap"
target_ids = ["hermes"]
tap_ms    = 250

[[binding]]
id        = "journal_hold"
label     = "Journal (Double-tap & Hold)"
keys      = ["KEY_LEFTMETA", "KEY_SPACE"]
gesture   = "double_tap_hold"
target_ids = ["journal"]
tap_ms    = 300
hold_threshold_ms = 200

Other fields: target_id (legacy singular target, resolved when target_ids is empty) and disabled (default false, disables a binding without removing it). LLM rewriting per binding is configured with the openai_enabled, openai_model, openai_mode, openai_prompt, and openai_system_prompt fields — the legacy ollama_* field names are still accepted as aliases.

Supported gesture values

Value Description
hold Record while key is held; stop on release
toggle First press starts; second press stops
double_tap Two presses within tap_ms window to activate
double_tap_hold Double-tap and keep held on the second press to start; release to stop