Repository navigation
Description
What happens
On Hyprland, Hex registers its actions with the compositor — hyprctl globalshortcuts lists anomaly.hex:dictation, anomaly.hex:paste-last, anomaly.hex:capture — but nothing binds a key to them.
So a user turns on dictation in Settings, presses the shortcut they recorded, and hears nothing. Making it work means discovering hl.dsp.global(...) and hand-writing a bind into their Hyprland config. Every other desktop Hex supports gets a working shortcut from Settings alone. This is not the same claim as the docs bug in #848 (the documented snippet used the old app id) — the current ids are correct, there is simply no bind on the user side of them.
Hex deliberately doesn't read /dev/input, so the compositor has to hold the keys. That leaves two routes: the GlobalShortcuts portal (what exists today, which Hyprland's portal advertises but does not bind), or the compositor-native one.
Current behaviour, concretely
- Settings records
dictation_hotkey/paste_last_hotkey/capture_hotkey; the portal listener starts and registers the actions by name. - Hyprland stores those names in its global-shortcut table and waits for a config bind that points at them.
- Nothing in Hex creates that bind, so the recorded key is inert until the user edits
~/.config/hypr/*.confthemselves. - Linux/Hyprland: Settings reports shortcut refused although compositor binding works #635 and Linux/Hyprland: changing the dictation shortcut silently breaks the compositor binding #1376 cover adjacent symptoms (Settings reporting the shortcut as refused, and the portal action name changing when the key changes). Neither gives the user a working key.
One approach: bind through hyprctl eval
Neutral description of a route that has been prototyped; not a claim that it is the right one.
For each shortcut recorded in Settings, emit one Lua bind through hyprctl eval:
hl.bind("ALT + F10", hl.dsp.global("anomaly.hex:dictation"))and keep the returned HL.Keybind handles so removal goes through the handle. hl.unbind(keys) is deliberately avoided for Hex's own binds — it removes every bind on those keys, including another application's. Details worth deciding on:
- When to bind. On start, and again when the recorded key changes, so a Settings edit takes effect without a restart.
- Conflicts. If another bind already holds the combination, leave it alone and log once per distinct conflict rather than shadowing it. On Omarchy the shipped
option+spaceis alreadyALT + SPACE→ "Toggle dictation", so the out-of-the-box result is a logged conflict and no bind until the user records a different key. - Binds left by an earlier run. A bind from a previous process has no live handle. Clearing it needs
hl.unbind(<combo>), which is destructive — only safe afterhyprctl -j bindsshows nothing else on that combination (same mask, same key compared case-insensitively, empty submap). - Reloads.
hyprctl reloaddrops IPC-created binds, so a periodic resync (5 s) plus a resync on the portalChangedevent is needed to notice and re-bind. - Cleanup. Remove every bind Hex made on stop, on change, and on re-bind.
- Timeout.
hyprctlchildren should be killed on a deadline, and a combo recorded as bound before the call, since a timed-out call may still have bound it. - Gating. This rewrites the compositor's binding table, so it fits AGENTS.md's Labs rule ("new or user-requested features land in Labs, off by default", gated where the work starts, not just in the UI) — behind
labs.hyprland_shortcuts, read where the listener starts so that with the flag off nothing toucheshyprctlat all.
The portal path stays unchanged on this route; the shortcut state machine and all four modes behave as they do today.
How a prototype of that approach behaves
- Binds on start and on Settings change; changing the key frees the old combo and binds the new one, no restart.
- Refuses a combination another bind already holds instead of stealing it, logging
ALT + SPACE is taken by Toggle dictationrather than silently shadowing. - Survives
hyprctl reloadvia a 5 s resync. - Removes its binds on quit; the bind table returns to its original entry count with the user's own binds untouched.
- Off by default behind
labs.hyprland_shortcuts. - Hyprland only; other compositors keep current behaviour.
How it was checked
Unit and repo checks pass on the prototype branch (scripts/check hyprland 23 passed; scripts/check 'linux::' 245 passed, 9 ignored; cargo test --locked --bin hex labs:: 55 passed; clippy -D warnings clean; cargo fmt --check clean; scripts/code-shape.py --check clean).
Live behaviour below was observed on one Hyprland 0.57.1 machine (Omarchy, Arch) with a locally built binary — it is a single-machine result, not coverage:
- Binds appear in
hyprctl -j bindsdescribedHex shortcut: anomaly.hex:dictation(ALT+F10),…:paste-last(CTRL+ALT+V),…:capture(F9), dispatchingHL.Dispatcher(global). - A real chord rather than a direct dispatch:
ALT+F10was synthesized through/dev/uinput, so it entered libinput as keyboard input and reached the bind table, then Hex's portal listener, with no config edit. - Switching
shortcut_modebetweenholdandtoggleinsettings.jsonchanged the resulting state without rebinding. - Editing
settings.jsonmoved a bind fromALT+SPACEtoALT+Jwithin ~8 s and freed the old combo. - SIGTERM left zero
Hex shortcut:binds.
Known limits of the prototype
- Only one machine, and no intelligible speech. The synthesized chord proves the bind path end to end, but the dictation sessions it produced were discarded by the voice-activity gate (
level_dbfs=-45.3,speech_threshold=0.4), so transcription of a spoken sentence over this path is unproven. Voice-activity calibration rarely transfers between machines and microphones, which is why this needs confirmation somewhere else before it counts. - Punctuation coverage is enumerated, not general. Keys with no xkb name are skipped with a warning rather than approximated.
COMMA,MINUS,EQUAL,BRACKETLEFT/BRACKETRIGHT,SEMICOLON,APOSTROPHE,GRAVE,BACKSLASH,PERIOD,SLASHwork. - The destructive case is real.
hl.unbind("<spec>")removes every bind on that combination — verified by watching it delete an unrelatedSUPER + comma"Dismiss, invoke, or focus last notification" bind during probing. It is case-insensitive on key and modifier names but requires modifier order SUPER, CTRL, ALT, SHIFT, and Hyprland will storeCOMMAandcommaas two separate binds on one physical key. - Registration isn't reflected back into Settings. With the portal's keyless grant, the row still reads "unconfirmed" while Hex's own bind works.
Prior art / reference implementation
A working prototype against dev at 8c900e8 lives on a branch in a fork: https://github.com/ThePsyberSleuth/hex/tree/hyprland-binds (494bbfc), 14 files. It is available as a PR if maintainers want it once direction is agreed; nothing has been sent upstream.
The ask
- Is the compositor-native route acceptable, or is the preference to keep pursuing the portal path (or to leave binding to the user's config and treat this as documentation)?
- If the route is acceptable, is it Labs-gated (
labs.hyprland_shortcuts, off by default) rather than on by default? - Should Hex ever clear a bind it did not create in this process, or is refusing and telling the user to remove it the safer default?
Activity
thdxr commentedon Nov 12, 2026
right now hyprland takes hex's shortcuts but leaves them unbound, so the key you record in settings does nothing until you add something like
bind = , End, global, anomaly.hex:dictationto your config. the linux docs say this, but settings only says it can't confirm the key, which doesn't help much.our thoughts:
hex dictationcommands bound in your desktop. having hex write binds into hyprland throughhyprctlwould be the first hyprland-only code there, so we want to decide that on purposetwo questions:
— from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊