Stop typing your prompts. Say them.
Press a key, talk, press it again — your words land on the clipboard, ready to paste into Claude Code, Aider, a commit message, anywhere.
Prompting an AI assistant well means writing a lot of prose, and you can speak about four times faster than you can type. quillm turns Neovim into a push-to-talk transcriber: it records your microphone, transcribes the audio with whisper.cpp entirely on your machine, and puts the text on your clipboard.
- 🎙️ One key —
<leader>qto start,<leader>qto finish - 🔒 Private — no API keys, no cloud, nothing leaves your machine
- ⚡ Fast — Metal on Apple Silicon, a second or two on any modern CPU
- 🪶 Tiny — a few hundred lines of Lua, zero plugin dependencies
With lazy.nvim:
{
"ibramsterdam/quillm.nvim",
build = "scripts/install.sh",
opts = {},
}That's the whole setup. The build step provisions whisper-cli (via
Homebrew when available, otherwise a one-time source build) and downloads
the speech model. Skipped the build step? quillm heals itself: the model
auto-downloads on first use, and :Quillm install runs the same
provisioning any time.
Then: hit <leader>q, say "Hey Claude, refactor the payment flow and add
specs", hit <leader>q again, and paste.
Verify everything with :checkhealth quillm.
| What | Linux | macOS |
|---|---|---|
| Recorder (auto-detected) | arecord, pw-record, ffmpeg, or sox — virtually every desktop has one |
ffmpeg or sox (brew install ffmpeg) |
| Speech-to-text | whisper-cli — provisioned by the build step |
same, via brew install whisper-cpp |
| Clipboard | xclip (X11) or wl-clipboard (Wayland) |
built in |
Neovim ≥ 0.10. For the source-build fallback you need cmake and a C
compiler; with Homebrew or a preinstalled whisper-cli, you don't.
On macOS, the first recording triggers the system microphone permission prompt for your terminal app — grant it once.
🎙 microphone ──▶ recorder (16 kHz wav) ──▶ whisper.cpp ──▶ "+ register ──▶ 📋 paste
<leader>q arecord/ffmpeg… local STT clipboard
No daemon, no background service. Recording spawns one process; stopping it
hands the WAV to whisper-cli; the result lands in a register. Everything
is async — Neovim never blocks.
| Mapping / command | Action |
|---|---|
<leader>q or :Quillm |
Toggle: start recording / stop + transcribe + copy |
:Quillm cancel |
Stop recording, discard the audio |
:Quillm install |
(Re)provision whisper-cli and the model |
:checkhealth quillm |
Diagnose recorder, transcriber, model, clipboard |
Defaults shown — pass only what you want to change:
require("quillm").setup({
keymap = "<leader>q", -- toggle key; false to map it yourself
register = "+", -- where the transcription lands ("+" = system clipboard)
-- whisper model; auto-downloaded here on first use
model = vim.fn.stdpath("data") .. "/quillm/ggml-base.en.bin",
language = "en", -- spoken language ("nl", "de", …) or "auto"; non-English needs a multilingual model
translate = false, -- translate the transcription to English (whisper's only target)
record_cmd = nil, -- function(wav) -> argv, overrides recorder auto-detection
transcribe_cmd = nil, -- function(wav) -> argv, replaces whisper-cli entirely
notify = true, -- progress notifications via vim.notify
})Another language. The default base.en model is English-only. Grab a
multilingual model (no .en in the name) from
huggingface.co/ggerganov/whisper.cpp,
point at it, and set the language:
opts = {
model = vim.fn.stdpath("data") .. "/quillm/ggml-small.bin",
language = "nl", -- or "auto" to detect per recording
}Speak Dutch, get English. Whisper translates any language to English
locally — add translate:
opts = {
model = vim.fn.stdpath("data") .. "/quillm/ggml-small.bin",
language = "nl",
translate = true,
}Better accuracy. small.en mishears noticeably less than base.en for
a modest speed cost — same setup, different file.
A specific microphone. Override the recorder:
record_cmd = function(wav)
return { "ffmpeg", "-loglevel", "quiet", "-f", "pulse",
"-i", "alsa_input.usb-your-mic", "-ar", "16000", "-ac", "1", wav }
end,A different STT engine. Anything that prints a transcript to stdout works:
transcribe_cmd = function(wav)
return { "my-stt", "--wav", wav }
end,Recording indicator in your statusline (lualine shown):
sections = {
lualine_x = {
function() return require("quillm").status() end, -- "● REC" while live
},
},- "Heard nothing" — whisper got silence. Check the right mic is the
system default, or pin one with
record_cmd. - Garbled technical terms — whisper guesses at jargon and file paths.
Upgrade to
small.en, or paste and fix the odd word; it's still faster than typing. - Wrong device on macOS — list inputs with
ffmpeg -f avfoundation -list_devices true -i ""and pin the index viarecord_cmd(-i ":1"). - Everything else:
:checkhealth quillmnames the missing piece and how to get it.
quill + LLM. A quill that writes down what you say, for the machine you're talking to.