Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

23 Commits

Folders and files

Repository files navigation

dictationapp

release AUR license: MIT

Push-to-talk dictation for Wayland, in the spirit of Wispr Flow. Hold a key, say what you mean, let go — the words land in whatever window you were using. Built for niri; powered by OpenRouter.

A small pill tracks the state — recording (with a live waveform), transcribing, cleaning up, typing — and closes completely when idle. The hotkey keeps working without an invisible window intercepting clicks or focus.

How it works

  1. Hotkey down — recording starts, with a subtle beep.
  2. Hotkey up — the audio is encoded to WAV and sent to OpenRouter's transcription endpoint.
  3. Cleanup pass (optional) — a second model pass fixes punctuation, capitalization, and obvious recognition errors without touching your meaning. If it fails, the raw transcript is used instead.
  4. Delivery — the result is typed into the focused window through a Unicode Wayland virtual keyboard and also placed on the clipboard, so nothing is lost if a field refuses synthetic input.

Install

Arch Linux (AUR)

paru -S dictationapp-bin   # prebuilt binary (fast)
paru -S dictationapp       # build from source

From source

git clone https://github.com/Derpedyea/niri-wispr
cd niri-wispr
cargo install --path .     # installs to ~/.cargo/bin/dictationapp
cd ui && bun install && bun run install-local   # the windows: ~/.cargo/bin/dictationapp-ui

The pill and Settings are GPUIX (React on Zed's GPUI) apps in ui/, compiled with Bun into one dictationapp-ui executable that the daemon starts per window.

To start it with niri:

spawn-at-startup "~/.cargo/bin/dictationapp"

Requirements

  • Wayland with layer-shell (niri, sway, Hyprland, …) — the pill is a bottom-anchored overlay surface
  • An OpenRouter API key — set it in the settings window, in config.toml, or via OPENROUTER_API_KEY
  • Read access to /dev/input/event* — for the global hotkey
  • A compositor supporting the Wayland virtual keyboard protocol (niri, sway, Hyprland, …) — for Unicode text insertion
  • wl-clipboard — publishes transcripts without focusing the pill; niri's data-control protocol lets the intended text field keep focus
  • pw-play, paplay, or aplay — for the start/stop cues (any one of them; beeps = false silences this entirely)
  • A microphone reachable through cpal's default input device

Granting /dev/input access is distro-specific — typically the input group plus a udev rule, or logind ACLs.

Usage

Run dictationapp once — it stays resident without an idle window and listens for the hotkey and for commands on a Unix socket ($XDG_RUNTIME_DIR/dictationapp-$USER.sock):

dictationapp --toggle     # start/stop dictation
dictationapp --start      # start recording
dictationapp --stop       # stop and transcribe
dictationapp --cancel     # throw the current recording away
dictationapp --settings   # open the settings window
dictationapp --reload     # re-read config.toml
dictationapp --quit       # exit
dictationapp --record <out.wav> [secs]   # save mic audio to a WAV (debug)

Clicking the pill also toggles recording. When the pill has focus: Space/Enter toggles, Esc cancels, Ctrl-Q quits.

Configuration

A microphone icon sits in your bar's tray (Noctalia, Waybar, KDE — anything that hosts StatusNotifierItems) and turns red while you're recording. Click it, or run dictationapp --settings, to open Settings: changes apply as you make them, and you set the hotkey by pressing it. Packaged installs also ship a Dictation Settings launcher entry, so it shows up in app launchers like fuzzel or the noctalia launcher. For a source install, copy it yourself:

install -Dm644 packaging/dictationapp-settings.desktop \
  ~/.local/share/applications/

The config lives at ~/.config/dictationapp/config.toml:

api_key = "sk-or-..."            # or set OPENROUTER_API_KEY (env wins)
model = "fish-audio/transcribe-1"
language = "en"                  # optional; omit to auto-detect
mode = "hold"                    # "hold" or "toggle"
hotkey = "KEY_RIGHTCTRL"         # any evdev key name, e.g. KEY_CAPSLOCK
# mic = "USB Microphone"         # input device name (exact or substring);
                                 # unset = system default. Pick it in Settings.
type_text = true                 # false = clipboard only
beeps = true                     # start/stop/error cues
cleanup = true                   # LLM pass that tidies the transcript
cleanup_model = "inclusionai/ling-3.0-flash"
  • hold — press and hold the hotkey; release to transcribe.
  • toggle — tap once to start, again to stop.

Output changes apply immediately, including transcripts still processing. Disabling typing stops any remaining insertion and keeps the full transcript on the clipboard. Changing the hotkey or mode discards an active recording. Clipboard-only completion briefly shows Copied. Paste to insert. A failed clipboard copy stops delivery and shows Copy failed. Check wl-clipboard.

In hold mode, taps under ¼ second are ignored. In either mode, using the hotkey in a shortcut (Right Ctrl+C) never starts a dictation.

niri setup

The pill needs no rules: it's a layer-shell overlay (namespace dictationapp-pill) anchored to the bottom of the focused output, it never takes keyboard focus, and it exists only while active or showing a message. Settings is an ordinary window; this rule floats it:

window-rule {
    match app-id="dictationapp" title="Dictation Settings"
    open-floating true
    min-width 460
    max-width 460
    min-height 760
    max-height 760
}

When idle there is no pill process at all, so nothing can cover buttons underneath it.

An optional compositor-level toggle:

binds {
    Super+D { spawn "dictationapp" "--toggle"; }
}

Troubleshooting

  • Nothing types — check that your compositor supports the Wayland virtual keyboard protocol. Transcripts still reach the clipboard; type_text = false silences the startup warning.
  • Copy failed — install wl-clipboard and check the Wayland session, then dictate again. This also supplies the fallback copy if typing fails.
  • Hotkey does nothing — check read access to /dev/input/event*. IPC (--toggle, clicking a visible pill) works regardless. The watcher picks up newly connected devices within about a second.
  • The hotkey isn't swallowed — there's no device grab (grabbing would break normal typing), so the key also reaches the focused app. Pick an inert key like Right Ctrl.
  • Hear what the model hears — dictationapp --record /tmp/speech.wav 5 and play it back.
  • Runtime logs go to stderr: recording started, transcript: ..., typed N chars, errors.

Releasing

The tag is the release — CI does the rest:

# bump `version` in Cargo.toml first, then:
git tag v0.2.0
git push origin v0.2.0

.github/workflows/release.yml builds the binary on Ubuntu, verifies the tag matches Cargo.toml, and publishes a GitHub release with a x86_64-linux-gnu tarball and checksums.

Auto-publishing to the AUR

The same workflow then updates two AUR packages — dictationapp (built from source) and dictationapp-bin (the release tarball) — via packaging/aur/publish.sh. One-time setup:

  1. Create an account at https://aur.archlinux.org/register.

  2. Generate a dedicated key pair:

    ssh-keygen -t ed25519 -f ~/.ssh/aur
  3. In your AUR account settings, add ~/.ssh/aur.pub under SSH Public Key.

  4. In this repo's Settings → Secrets and variables → Actions:

    • Secret AUR_SSH_PRIVATE_KEY — contents of ~/.ssh/aur (required)
    • Secret AUR_USERNAME — your AUR username (optional; commit author)
    • Variable AUR_EMAIL — your AUR email (optional; commit author)
  5. Push a tag. On the first run the packages are created automatically (pushing to a nonexistent pkgbase creates it), so dictationapp and dictationapp-bin get claimed under your account.

If AUR_SSH_PRIVATE_KEY isn't set, the AUR job skips itself — the GitHub release still happens.

Development

cargo build                    # debug binary at target/debug/dictationapp
cargo test                     # deterministic tests; no desktop input
cargo test -- --ignored        # opt-in hardware/input + network tests
cd ui && bun install && bun run typecheck   # debug builds run ui/src with bun
python3 scripts/test-aur-publish.py  # mocked publishing; no network or credentials

Rust daemon: cpal (audio), hound (WAV), evdev/uinput (hotkey + typing), ureq (HTTP). Windows: GPUIX React apps in ui/, one process per window, driven over stdin/stdout. See docs/window-lifecycle.md for native verification.

File Role
main.rs CLI dispatch, wiring
app.rs command loop + state machine (Idle → Recording → Transcribing → Cleaning → Typing), pill view
ui.rs starts window processes, JSON views in / events out
ui/src/ the pill and Settings in React (GPUIX)
audio.rs mic capture + WAV encode
api.rs OpenRouter transcription + cleanup pass
hotkey.rs evdev global hotkey watcher
typer.rs Unicode insertion through a Wayland virtual keyboard
ipc.rs Ordered CLI commands, acknowledgements, and lifetime-held instance lock
settings.rs settings model: saves, reloads, key capture
tray.rs tray icon in the bar
beep.rs audio cues
niri.rs focuses Settings via niri msg
config.rs config.toml load/save
packaging/aur/ PKGBUILDs + the AUR publish script

License

MIT © Derpedyea

dictationapp-ui bundles GPUIX (Apache-2.0, which includes Zed's GPUI), React (MIT) and the Bun runtime.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages