I wanted dictation on Linux. Not a web page with a microphone button — the thing where you hold a key, talk, let go, and the words appear in whatever you were already typing into. That product exists and is good on macOS and Windows. On Linux it mostly doesn’t.
I assumed the reason was the model. It is not. I had transcription working in an afternoon, and the remaining fortnight went entirely on the last eight inches: moving text from my process into someone else’s window.
01The easy part, first
NVIDIA Parakeet TDT runs offline through ONNX Runtime and transcribes eleven seconds of speech in 36 milliseconds on a consumer GPU — about 307× faster than real time. It punctuates and capitalises without help. There is no cloud in the path and no account to make.
$ murmur transcribe assets/jfk.wav --repeat 5
model parakeet-tdt-0.6b-v3 on cuda
audio 11.00s
warm 35.8ms median of 5 (307x realtime)
And so, my fellow Americans, ask not what your country can do for you.
Ask what you can do for your country.
That is the whole machine-learning story. Everything below is about the other end.
02The obvious approach does not work on GNOME
Every dictation tool does the same thing: put the text on the clipboard, synthesise
Ctrl+V, restore the clipboard. It is fast, it is Unicode-exact, and it works
everywhere.
It cannot be done on GNOME. Setting the clipboard from a process that does not hold keyboard focus requires a data-control protocol, and mutter implements neither of the two that exist:
$ murmur doctor
✓ uinput /dev/uinput is writable
✗ clipboard unavailable (A required Wayland protocol
(ext-data-control, or wlr-data-control version 1)
is not supported by the compositor)
This is not a bug and it is not going to be fixed by waiting. Reading and writing the clipboard without focus is exactly the capability a keylogger wants, and GNOME has consistently declined to expose it. wlroots compositors and KDE support it; GNOME does not.
If your plan for getting text into an application is the clipboard, your plan does not work for most Linux desktop users.
03What works is going underneath
The compositor controls Wayland. It does not control the kernel. /dev/uinput
lets you create a virtual input device, and the events you emit arrive through evdev exactly
like events from a real keyboard — libinput cannot distinguish them, so neither can the
compositor, the toolkit, or the application.
let mut keys = AttributeSet::<KeyCode>::new();
for key in keymap::all_keys() { keys.insert(key); }
let device = VirtualDevice::builder()?
.name("Murmur virtual keyboard")
.with_keys(&keys)?
.build()?;
Two things bite immediately.
The device takes time to exist
libinput discovers input devices through udev, asynchronously. Events emitted in the first few hundred milliseconds after creation are delivered to nobody. The fix is not a retry loop — it is to create the device once at start-up and hold it open for the life of the process, so the delay is paid before the user ever presses the key.
You emit scancodes, not characters
A keyboard does not send letters. It sends key positions, and the compositor applies the
keymap. So you cannot type a character the user’s layout has no key for, and you
cannot know their layout from outside the compositor. For ASCII on a common layout this is
fine. For é, or an emoji, or a Dvorak user, it is not.
The escape hatch on GNOME is the RemoteDesktop portal, which takes keysyms
rather than scancodes and therefore handles all of Unicode. It costs one consent dialogue,
and a restore token makes that once rather than every launch. It is the right long-term
path; uinput is what works with no prompt at all.
04You cannot swallow one key
Push-to-talk needs to know when a key goes down and when it comes up. Wayland has no portable global-hotkey API, and the one GNOME does offer reports activation, not edges — which is the wrong shape for hold-to-talk. Reading evdev directly gives you both edges on every keyboard in any session.
What it does not give you is the ability to consume the key. EVIOCGRAB is
all-or-nothing per device: you can take exclusive control of an entire keyboard, or
none of it. There is no interface for “give me this one key and pass the rest
through”.
Consequence: your trigger key must be one that does nothing on its own, because the application underneath will receive it too. That single kernel constraint is the entire reason the default is Right Ctrl — no desktop binds it alone, it is reachable one-handed, and an application that sees a bare Ctrl press does nothing with it.
It is also worth being honest about what reading evdev requires. Access to
/dev/input/event* is the ability to read every keystroke on the machine. systemd
grants that to the active session only for joysticks; keyboards need group membership. A
package can ship the udev rule, but it should not quietly add you to the group — that is a
decision, not an installation step.
05An overlay that takes focus types into itself
Then there is the small window showing what you are saying. It has one hard requirement: it must never take keyboard focus. Text is injected into whatever the compositor considers focused, so a HUD that steals focus receives its own dictation.
Wayland gives a client no way to refuse focus. There is no
override-redirect, no _NET_WM_STATE, no request that means
“show this but never activate it”. The protocol built for exactly this —
wlr-layer-shell — is not implemented by mutter, and there is no shipped
standard replacement.
Worse, a Wayland client cannot position its own window at all. xdg-shell has
no concept of a coordinate; placement belongs entirely to the compositor, and GNOME places
new windows in the middle of the screen. Which is precisely where an overlay must not be.
The workaround is unglamorous and effective: run the overlay through XWayland, where absolute placement still works.
// Only the driver is privileged; everything else is userspace.
// Nothing else Murmur does touches Wayland: injection is uinput,
// the trigger is evdev, and audio is ALSA.
if env::var_os("WAYLAND_DISPLAY").is_some() && env::var_os("DISPLAY").is_some() {
unsafe { env::remove_var("WAYLAND_DISPLAY") };
}
And then a second surprise: “centred” is not the centre. X11 reports two 1920×1080 monitors as one 3840×1080 screen, so centring on it puts the bar exactly on the bezel. There is no monitor list available at placement time, but the panel count can be inferred by assuming a conventional aspect ratio and seeing how many fit — which leaves an ultrawide correctly undivided, because 3440×1440 is nowhere near twice 16:9.
06What this cost, in one table
| Wanted | Obvious way | What actually works |
|---|---|---|
| Type into any app | Clipboard + paste | uinput virtual keyboard |
| Full Unicode | Clipboard | RemoteDesktop portal keysyms |
| Global push-to-talk | Compositor hotkey API | evdev, key that is harmless alone |
| Always-on-top overlay | layer-shell | XWayland window, never re-mapped |
| Place the overlay | Set its position | XWayland, or let the user drag it |
Not one row of that table is about speech.
07What I would want from Wayland
None of this is mutter being obstructive for its own sake. Every capability I wanted is also a capability malware wants, and the reason X11 made them easy is the reason X11 was indefensible. The gap is not that the answer is “no” — it is that there is no way to ask.
- A permissioned synthetic-input path that is not remote desktop. The portal exists and works; it is scoped for screen sharing, and using it to type one sentence is a heavy tool for a small job.
- A window type that declares it will never accept focus. Overlays, HUDs and on-screen keyboards all need it, and every one of them currently reaches for XWayland.
- Placement for those windows. Not general client positioning — just enough for a surface that has already said it is an overlay.
Until then: go below the compositor for input, sideways through XWayland for the window, and be honest in your documentation about what you are asking users to grant.
Murmur is the thing this came out of — local voice typing for Linux, in Rust, with
NVIDIA Parakeet through ONNX Runtime. 36 ms from key release to text, and nothing leaves
the machine. Apache-2.0, .deb and .rpm.