← All notes
Field note · Voice tooling

A dictation shortcut that knows when to keep listening

How I combined Voxtype, Hyper+W, and Hyprland submaps into a reliable voice loop for agent chats.

Omarchy is an opinionated Arch Linux and Hyprland system that ships its configuration as readable source. That is where open source gets much more interesting with agents. The old Linux tax of debugging, searching forums, and stitching config together can now be delegated. I get a blazing fast OS from someone who knows and loves the stack, plus the recipe. An agent can inspect that recipe and adapt it to me. This shortcut is one example of the behavior I wanted.

Current models know to reach for compositor primitives such as submaps. Opus 4.5 did not seem to make that jump for me. OmarchySTT covered basic dictation, but I hit its limits once I wanted one key to support a hold action, a persistent loop, and mode-specific controls.

I wanted one Voxtype shortcut with two modes: hold it for ordinary dictation, or tap it to start a hands-free loop with an agent chat.

The result uses Hyper+W, a small controller script, and two Hyprland submaps.

The setup

Input Result
Hold Hyper+W for one second Record, then transcribe and paste on release
Tap Hyper+W Start continuous agent-chat dictation
Enter Transcribe, paste, submit, then record again
Tap Super Transcribe, paste, submit, and stop
Hyper+W again Transcribe and paste without submitting, then stop
Escape Cancel
F12 Recover to the default state

Hyper means Super+Shift+Control+Alt. That chord is ugly on four physical keys, so keyboard nerds usually map it to one otherwise-underused key, often Caps Lock. On Linux, Kanata is a common way to do that. Karabiner-Elements fills the same niche on macOS.

The physical remap is optional. Hyprland only sees the resulting modifier chord.

Why submaps

A Hyprland submap is a temporary keyboard mode: basically a Vim mode at compositor level.

Voxtype can transcribe audio, but it should not globally steal Enter from an arbitrary Wayland application. Hyprland already owns keyboard routing, so I let it consume the control keys while recording:

default
  Hyper+W

voxtype_recording
  Enter / Escape / Super / Hyper+W are control keys
      ↓ transcription completes
voxtype_suppress
  held physical modifiers cannot corrupt the paste

default

This prevents the classic race where Enter submits an empty composer before transcription finishes.

Minimal configuration

I disabled Voxtype’s own hotkey and used clipboard paste instead of simulated typing:

# ~/.config/voxtype/config.toml
[hotkey]
enabled = false

[output]
mode = "paste"
paste_keys = "shift+insert"
restore_clipboard = false
pre_type_delay_ms = 100
auto_submit = false

pre_recording_command = "/home/USER/.local/bin/voxtype-paradict submap voxtype_recording"
pre_output_command = "/home/USER/.local/bin/voxtype-paradict submap voxtype_suppress"
post_output_command = "/home/USER/.local/bin/voxtype-paradict submap reset"

wl-copy owns the clipboard; wtype sends Shift+Insert. Submission stays in the controller, so normal hold-to-dictate never sends Enter.

The Hyprland side lives in ~/.config/hypr/bindings.lua. The important part is not the exact Lua wrapper. It is splitting press from release and keeping the release binding alive across submaps:

local HYPER = "SUPER + SHIFT + CTRL + ALT"
local controller = "/home/USER/.local/bin/voxtype-paradict"

hl.bind(HYPER, "W", "exec", controller .. " press")
hl.bindr(HYPER, "W", "exec", controller .. " release", {
  submap_universal = true,
})

hl.define_submap("voxtype_recording", function()
  -- Hyprland 0.56.2 silently omits symbolic RETURN in this named submap.
  hl.bind(
    "code:36",
    hl.dsp.exec_cmd(controller .. " submit-continue")
  )
end)

On Hyprland 0.56.2, a symbolic RETURN binding inside the named recording submap was silently omitted. hyprctl configerrors remained empty, so config validation did not catch it. code:36 binds the physical main Enter key. Keypad Enter needs a separate binding. After every reload, inspect hyprctl binds and confirm this binding exists under voxtype_recording.

The controller distinguishes tap from hold with monotonic time, serializes transitions with flock, waits for Voxtype state changes, and resets Hyprland on every error path.

The release-order bugs

A submap solves routing, not every modifier edge case. I needed three extra guards:

  • Universal release: recording changes the active submap before W is released.
  • Super arming delay: releasing the Hyper chord must not look like an intentional bare-Super finish gesture. I ignore Super releases for 400 ms after a tap.
  • Output suppression: transcription can finish while a modifier is still physically down. The suppression map consumes those modifiers during paste and submit.

The stuck activation bug

One release order still broke the loop. If I released Hyper before W, Hyprland no longer considered the later W release a Hyper+W event. The controller kept its activation marker, so Super and Enter appeared stuck.

The fix was to let either edge finish activation. If a Super release arrives while the Hyper+W marker still exists, the controller now claims that marker and performs the same tap-or-hold transition as a W release. It does not submit on that same event, so a later bare Super or Enter keeps its intended meaning.

Together, these guards make modifier release order irrelevant. That is the part most dictation shortcuts get wrong.

Recovery is intentionally boring:

~/.local/bin/voxtype-paradict recover
systemctl --user restart voxtype.service

Agent instructions to copy

Set up Voxtype on my Omarchy system as follows:

- Use the installed Omarchy/Hyprland conventions. Inspect the live system,
  current bindings, Voxtype version/config, and relevant command help before
  editing. Never edit /usr/share/omarchy; only edit user-owned config.
- Back up every file you change. Preserve unrelated shortcuts. Inspect current
  bindings with `omarchy menu keybindings --print`, and explicitly unbind any
  conflicting Hyper+W or stock Voxtype binding before replacing it.
- Hyper means Super+Shift+Control+Alt. Bind Hyper+W press and release in
  ~/.config/hypr/bindings.lua. Do not configure my physical Hyper/Caps Lock
  remap unless I ask; it may already be handled by Kanata.
- Hold Hyper+W for at least 1 second: record while held, then transcribe and
  paste on release without submitting.
- Tap Hyper+W: keep recording. In this mode, Enter must transcribe, paste,
  submit once, and immediately record again. Bare left or right Super must
  transcribe, paste, submit, and stop. Hyper+W again must transcribe and paste
  without submitting, then stop. Escape cancels. F12 performs recovery.
- Disable Voxtype's own hotkey. Configure clipboard output with mode="paste",
  paste_keys="shift+insert", restore_clipboard=false,
  pre_type_delay_ms=100, and auto_submit=false. Verify wl-copy and wtype exist.
- Implement a small ~/.local/bin/voxtype-paradict controller and two Hyprland
  submaps: voxtype_recording for control-key routing and voxtype_suppress for
  safe output while physical modifiers may still be held.
- On Hyprland 0.56.2, bind the physical main Enter key inside
  voxtype_recording as code:36, not symbolic RETURN. Add a separate keypad
  Enter binding only if needed.
- Make the Hyper+W release binding universal across submaps. Add a 400 ms
  arming delay so releasing the initiating Hyper chord cannot trigger the bare
  Super stop action. Serialize transitions with flock, use monotonic timing,
  wait for Voxtype/Hyprland state changes, and reset both on errors.
- Validate with `hyprctl reload` and `hyprctl configerrors`. Then inspect
  `hyprctl binds` and confirm code:36 exists under voxtype_recording. Restart
  the Voxtype user service, and report the final bindings, files, backups, and
  recovery command. Do not run live paste/submit tests into the active agent
  terminal; use a disposable text field or leave that final interactive test
  to me.

The useful abstraction is simple: Voxtype owns audio, Hyprland owns keys, and the controller owns state.

Add to the notes

No account required. A honeypot and rate limits keep the obvious bots out; thoughtful humans are welcome.

Loading comments…