Say it, and the window manager does it.
Omarchy already turns your voice into text. Desktop Voice Control turns it into actions. Hold
F10, say "focus left", "workspace three", "throw this to two", "lock the
screen" — and it happens.
Everything runs on your machine. Audio is recorded with pw-record,
transcribed by Voxtype (whisper.cpp), matched against a
grammar you can edit, and then discarded. No network, no wake word, no
always-on microphone: the mic opens when you hold the key and closes when you
let go.
Dictation types what you said into the focused window. Desktop Voice Control never types anything. An utterance either matches a command in the grammar and runs it, or it is thrown away. That is the whole safety model, and it means a misheard sentence can't end up in your code.
F9 dictates words. F10 gives commands. Same hand, same gesture, different verb.
omarchy plugin add https://github.com/RR-CodeBase/omarchy-utter.git --enableTwo things the plugin cannot do for you, both one command:
omarchy voxtype install # local transcription
~/.config/omarchy/plugins/io.github.rr-codebase.utter/install.sh # the F10 bindinginstall.sh is safe to re-run and only ever touches its own managed block in
~/.config/hypr/bindings.lua. It also links utter into ~/.local/bin and
installs bash completion, which is what makes the bare utter … commands below
work. Working from a clone instead? git clone, then ./install.sh — it
registers the plugin, places the widget and adds the binding in one go.
Hold F10, say what you want, release. Click the bar icon for the current state, what was heard last, and an on/off switch; Escape closes the panel. Middle-click the icon to turn voice commands off without opening anything.
utter commands # everything it understands
utter doctor # check the install
utter say "focus left" # try a command without speakingomarchy bar move io.github.rr-codebase.utter --section rightThe grammar is a JSON file you own at ~/.config/omarchy/utter/commands.json,
and settings live beside it. Both are covered below.
~/.config/omarchy/plugins/io.github.rr-codebase.utter/install.sh --uninstallThat removes the keybinding block, the utter symlink, the completion and the
plugin. omarchy plugin remove io.github.rr-codebase.utter on its own removes
the plugin but leaves the binding behind. Your grammar and history stay in
~/.config/omarchy/utter/ and ~/.local/state/omarchy/ either way; delete
those directories to go back to nothing.
utter commands # the full list, grouped37 commands out of the box, covering windows, workspaces, apps, sound, display, focus modes, capture and session. A few of them:
| Say | Runs |
|---|---|
| "focus left" / "go right" | hl.dsp.focus({ direction = "l" }) |
| "workspace three" | hl.dsp.focus({ workspace = "3" }) |
| "throw this to two" | hl.dsp.window.move({ workspace = "2" }) |
| "close window" | hl.dsp.window.close() |
| "open teams" / "focus on teams" | focuses it if open, launches it if not |
| "take a screenshot" | omarchy capture screenshot region |
| "do not disturb" | omarchy toggle notification silencing |
| "lock the screen" | omarchy system lock |
Anything destructive — reboot, shut down, sleep — has to be said twice within eight seconds before it runs.
The grammar lives at ~/.config/omarchy/utter/commands.json and is yours.
Add a command by adding an entry:
{
"id": "notes.open",
"group": "Apps",
"say": ["open my notes", "show my notes", "notes"],
"run": "omarchy launch or focus obsidian",
"label": "Open notes"
}Slots let one entry cover many phrasings. {dir} and {num} ship by default,
and you can add your own under slots:
{
"id": "focus.move",
"say": ["focus {dir}", "go {dir}"],
"run": "hyprctl dispatch movefocus {dir}",
"label": "Focus {dir}"
}A slot maps what you say to what gets run: "l": ["left"] means saying
"left" substitutes l. Only the canonical value ever reaches the command line,
so what you say can never inject arguments.
"open anything", and equally "focus on anything", is resolved against the apps actually installed on your
machine — the .desktop entries a menu would show you — matching on the app's
name, its binary, its window class and its file id, and tolerating how speech
comes out ("one password" finds 1Password, "x journal" finds Xournal++). If
nothing matches well enough you get "no app called …" rather than the nearest
alphabetical guess, because opening the wrong app is worse than opening none.
If the app already has a window open, Desktop Voice Control focuses it rather than starting a
second copy, and says which it did — "Focus Teams" against "Open Teams".
The window is found by address from hyprctl clients, matched on the app's
declared window class, its binary and its name, because handing a pattern to a
launcher gets web apps wrong: Teams' Exec is omarchy-launch-webapp …, and
matching windows against that finds nothing and opens a duplicate.
"focus left" stays a direction and "focus teams" becomes an app: where both could match, a known value beats a wildcard. And a near miss on a known command beats a wildcard too, so "focus lefd" is still corrected to left rather than being taken as an app nobody has.
Phrases that name a role rather than an app — "browser", "terminal",
"editor" — go through Omarchy's configured default, so they open yours. That
table lives in the grammar under appAliases and is yours to extend:
"appAliases": {
"my notes": { "app": "obsidian" },
"the shell": { "run": "omarchy launch terminal", "label": "terminal" }
}This is the one place free speech enters the system, so it is fenced: a free
slot ({appname}) may only be used by a command with an internal handler,
never by one with a run line, and the handler launches a resolved desktop
entry rather than anything the microphone heard. utter doctor enforces both
rules, and the test suite asserts them over the whole grammar.
Hyprland commands go through its Lua dispatcher API (hl.dsp.…), not the
older hyprctl dispatch workspace 3 form — that one is accepted by the CLI
and rejected by the compositor, so it looks like it worked. utter doctor
proves the calling convention against the running compositor and refuses to
let the legacy form back into the grammar.
After editing, utter doctor re-checks the whole file.
The grammar carries a version, and Desktop Voice Control records the fingerprint of the file
it installed. When a newer grammar ships:
- if your file is still exactly what it wrote, it is replaced and the old
one kept beside it as
commands.v<n>.json; - if you have edited it, it is left alone. The new grammar is written
beside it as
commands.new.jsonandutter doctortells you it is there.
An upgrade never walks over your edits, which matters because a stale grammar can be silently broken — the v1 one drove Hyprland with a syntax this Hyprland rejects, so its commands matched, reported success, and did nothing.
Two passes. First an exact match of the normalized utterance against every phrase in the grammar — punctuation, capitalization, filler words ("um", "please") and wake words ("omarchy", "computer") are stripped first. That handles most clean transcriptions in microseconds.
If nothing matches exactly, a fuzzy pass scores the utterance against every
phrase and accepts the best one above threshold (0.80 by default). This is
what rescues "focus lefd" and "next workspase". Below the threshold you get
"Didn't catch that" and nothing runs — including for ordinary conversation
picked up by a hot mic, which the test suite pins down explicitly.
Whisper's habit of writing digits, and its homophones, are handled in the
grammar itself: "2": ["two", "to", "too", "2"].
utter ptt start|stop push-to-talk, for keybindings
utter listen toggle recording
utter say "focus left" interpret typed text as if spoken
utter match "focus left" match without running (add --threshold)
utter commands [--json] list the grammar
utter status [--json] what the widget reads
utter history recent utterances and what they matched
utter enable|disable|toggle
utter doctor check the whole install
utter selftest record 3s, transcribe, match
utter say is useful beyond testing: bind it to a key and you have a text
command palette that shares the grammar.
~/.config/omarchy/utter/settings.json
| Key | Default | |
|---|---|---|
enabled |
true |
master switch |
threshold |
0.80 |
fuzzy match floor; raise it if commands misfire |
maxSeconds |
8 |
recording safety limit |
osd |
true |
show the Omarchy OSD on each result |
notify |
false |
also send a notification |
stripWakeWords |
true |
ignore a leading "omarchy" / "computer" |
voxtype |
"" |
path to a specific voxtype binary |
model |
"" |
override the whisper model |
- Omarchy Quattro
pipewire(pw-record)- Voxtype and a whisper model —
omarchy voxtype install - Python 3.11+ (standard library only)
python3 tests/test_grammar.py # the matcher and the grammar
python3 tests/test_pipeline.py # real audio through whisper to a process
python3 tests/test_plugin.py # conformance with the Omarchy plugin guide215 assertions covering slot canonicalization, homophones, transcription noise,
fuzzy tolerance, argument-injection safety, a corpus of ordinary speech that
must never match a command, the app resolver, the rule that free speech never
reaches a command line, and the phrasings real use turned up. Plus
tests/test_pipeline.py, which drives real audio through whisper into the
matcher and out to a process.
MIT. See LICENSE, and NOTICE for what this builds on.