- Go 99.1%
- Shell 0.9%
wlrun
wlrun — apps mode with the bordered theme
A keyboard-driven launcher and system console for
river (and any other Wayland compositor
that speaks wlr-layer-shell). One self-contained Go binary covers app
launching, network management (Wi-Fi + wired + OpenVPN), Bluetooth,
display setup, removable storage, keyboard layouts, and a pass password
store browser — crisp FreeType-hinted text, no GTK/Qt runtime.
It is not a generic, scriptable dmenu/rofi/fuzzel replacement: wlrun has a fixed, curated set of modes that each do one job, built around two hard rules — no GTK/Qt dependency and the smallest dependency tree and feature set that still gets the job done. See CONTRIBUTING.md for the project scope and non-goals.
🤖 Vibe-coded with Claude.
The built-in modes:
| Mode | What it does | Backend |
|---|---|---|
menu |
All-in-one hub (the default) — every mode below in one list; Enter switches in, Tab jumps back here from any mode |
— |
apps |
Scan $PATH for executables, fuzzy-filter, exec the pick (or run free-form prompt) |
$PATH |
network |
Wi-Fi + wired (DHCP / static / DNS) + OpenVPN configs in one menu | iwctl, iproute2, openvpn |
bluetooth |
List paired devices, scan & pair, connect / disconnect / forget | bluetoothctl |
display |
Per-output toggle, mode / scale / transform, position presets | wlr-randr |
mount |
Mount / unmount / eject removable block devices and gvfs volumes (optional format), plus sshfs / nfs / cifs / custom network mounts from mounts.toml |
udisksctl, gio, sshfs / mount (per profile) |
keyboard |
Switch xkb layout (from [[keyboard.layouts]]), live layout flagged (current) |
riverctl / swaymsg / hyprctl (configurable) |
pass |
Browse the password store, copy passwords + TOTP codes + saved logins | pass + pass-otp |
Most modes track per-pick usage frequency, so daily-driver SSIDs, apps,
devices and entries float to the top. wifi is a CLI alias for network.
Install
Arch Linux (AUR)
yay -S wlrun # latest tagged release
yay -S wlrun-git # builds from main
The AUR package pulls only the base deps (libxkbcommon, freetype2,
fontconfig); every per-mode tool is an optdepends, so install what you
need for the modes you use.
From source
Needs cgo and the freetype2 development headers (Arch: freetype2;
Debian/Ubuntu: libfreetype6-dev + pkg-config):
go build -o wlrun ./cmd/wlrun
# or: go install codeberg.org/vighd/wlrun/cmd/wlrun@latest
Requirements
- A Wayland compositor with
wlr-layer-shell-unstable-v1(river, sway, Hyprland, niri, ...) libxkbcommon(dlopen'd) andfreetype2(linked — also needed at build time, since glyphs are rasterised through FreeType for sharp hinted text)- A TTF/OTF font reachable by fontconfig
Per-mode tools, only for the mode you actually use:
| Mode | Tools |
|---|---|
network Wi-Fi |
iwd (iwctl) |
network wired |
iproute2, dhcpcd/dhclient, systemd-resolved, sudo |
network VPN |
openvpn, sudo |
bluetooth |
bluez + bluez-utils |
display |
wlr-randr |
mount |
udisks2, optionally gvfs (MTP / phones) |
keyboard |
riverctl (river), or swaymsg / hyprctl / niri |
pass |
pass, optionally pass-otp |
| Ctrl+V paste | wl-clipboard (Wayland) or xclip (X11) |
| notifications | libnotify |
sudo covers the wired-network and OpenVPN privilege escalation —
wlrun captures the password in its own UI, so no polkit agent is needed.
Usage
wlrun # all-in-one hub (default)
wlrun apps # application launcher
wlrun network # Wi-Fi + wired + OpenVPN (alias: wlrun wifi)
wlrun bluetooth # Bluetooth manager
wlrun display # per-output toggle / mode / position
wlrun mount # removable block devices + gvfs volumes
wlrun keyboard # xkb layout switcher
wlrun pass # password store
wlrun --print-config # dump effective config
# One-shot, no UI — replace way-displays / kanshi from a river init script:
wlrun display eDP-1 1920x1200 60 0,0
wlrun display HDMI-A-1 off
Typical river init:
riverctl map normal Super Space spawn 'wlrun'
riverctl map normal Super D spawn 'wlrun apps'
riverctl map normal Super N spawn 'wlrun network'
Key bindings
| Key | Action |
|---|---|
| Type | Filter the list |
↑ / ↓, Ctrl+P / Ctrl+N |
Move selection |
Tab |
Jump back to the hub to switch mode |
Enter |
Confirm the highlighted item |
Backspace / Ctrl+W / Ctrl+U |
Delete char / word / line |
Ctrl+V |
Paste clipboard into the active input |
Esc / Ctrl+C |
Dismiss; while a background action runs, cancel it (press again to force-quit) |
Held keys repeat at the compositor's configured rate/delay.
While a long-running action shows its spinner (mounting a network share,
connecting a VPN, scanning or pairing Bluetooth, associating Wi-Fi...), Esc /
Ctrl+C cancels it — the underlying subprocess is killed and any poll/wait
abandoned — and returns you to the menu rather than leaving you locked behind
the keyboard grab. If a worker is genuinely wedged and doesn't unwind, a second
Esc / Ctrl+C force-quits the launcher, so the grab is always releasable.
Configuration
wlrun reads ~/.config/wlrun/config.toml (or
$XDG_CONFIG_HOME). Everything has sensible defaults — set only what you
want to override. See config.sample.toml for the
full schema and examples/ for ready-made themes.
[font]
primary = "JetBrainsMono Nerd Font"
size_px = 14
[window]
layout = "window" # or "bar" for a dmenu-style top strip
width = 600
max_rows = 12
border_color = "#fabd2f"
border_radius = 8
opacity = 0.9 # translucent background; text/border stay opaque
[colors]
background = "#282828"
foreground = "#ebdbb2"
prompt = "#fabd2f"
selected_bg = "#fabd2f"
selected_fg = "#282828"
[icons]
enabled = true # Nerd Font glyphs on mode entries
[notifications]
enabled = true
One accent hue is shared across the prompt, title, fuzzy-match highlight,
scrollbar thumb and every active-state marker (connected Wi-Fi/Bluetooth,
the (current) keyboard layout, the preferred display mode), so a single
colour change re-themes the whole launcher.
window.opacity (0.0–1.0, default 1.0) makes only the background
translucent so the desktop shows through, while text, border and icons
stay fully opaque on top — the frosted-terminal look. Needs a compositor
that honours surface alpha.
base16 schemes
Instead of hand-tuning the palette, seed it from a base16 scheme:
[theme]
base16 = "gruvbox-dark-medium"
The sixteen colours are spread across UI roles (blue title + selection,
cyan prompt, green connected, red match, magenta scrollbar) so the scheme's
identity shows. Any [colors] key still overrides its role.
wlrun ships no schemes — drop a .yaml into
~/.config/wlrun/themes/<name>.yaml and reference it by <name>.
Both tinted-theming/schemes
(palette:) and the archived
base16-schemes (flat)
formats are read.
Themes
Drop any of these into your config, or merge the bits you like. Each
previews the apps mode mid-search.
| Theme | Preview |
|---|---|
| base16 — whole palette from one scheme name (toml) | base16-gruvbox |
| gruvbox — the default, made explicit (toml) | gruvbox |
| nord — cool slate + cyan accent (toml) | nord |
| catppuccin-mocha — pink prompt, green selection (toml) | catppuccin-mocha |
| bordered — rounded border + icons (toml) | bordered |
| minimal — monochrome, single accent (toml) | minimal |
| bar — bemenu-style top strip (toml) | bar |
Keyboard layouts (any compositor)
The keyboard mode runs a configurable command template on pick, so it's
not tied to river. The live keymap is probed and the match flagged
(current).
[keyboard]
# {layout}/{variant}/{options} expand to the picked entry; an empty
# placeholder drops the word before it. Empty falls back to the riverctl form.
# River: "riverctl keyboard-layout -options {options} -variant {variant} {layout}"
# Sway: "swaymsg input type:keyboard xkb_layout {layout}"
# Hyprland: "hyprctl keyword input:kb_layout {layout}"
# niri: "niri msg action switch-layout {layout}" (see note below)
set_command = "riverctl keyboard-layout -options {options} -variant {variant} {layout}"
layouts = [
{ name = "English (US)", layout = "us" },
{ name = "Hungarian", layout = "hu" },
{ name = "Programmer Dvorak", layout = "us", variant = "dvp" },
]
niri switches between layouts already declared in its config by index,
not by xkb name — so put the 0-based index in layout and let name carry
the label (variant / options / (current) don't apply):
# niri config has: input { keyboard { xkb { layout "us,hu" } } }
[keyboard]
set_command = "niri msg action switch-layout {layout}"
layouts = [
{ name = "English (US)", layout = "0" },
{ name = "Hungarian", layout = "1" },
]
Inline GPG passphrase prompt (optional)
By default the pass mode lets GPG spawn pinentry. To type the passphrase
straight into wlrun's own secret-input UI instead:
[pass]
prompt_passphrase = true
This needs GPG's loopback-pinentry mode (one-time setup):
echo allow-loopback-pinentry >> ~/.gnupg/gpg-agent.conf
gpgconf --kill gpg-agent
The passphrase is forwarded to gpg over an inherited pipe; a wrong one
re-prompts instead of closing the launcher.
Once entered, the passphrase is cached by gpg-agent for its
default-cache-ttl (10 minutes by default), and wlrun probes that cache
before every grab. So on an MFA login you type the passphrase once for the
password and the follow-up OTP grab reuses the cached passphrase with no
second prompt. Tune the window with e.g. default-cache-ttl 3600 in
~/.gnupg/gpg-agent.conf. wlrun also leads each entry's Password/OTP/Login
sub-menu with the action you most likely want next: copying the password of an
entry that also holds an otpauth:// URI promotes OTP to the top for the next
visit (once the OTP is taken it resets to the password), so a repeat MFA login
is just Enter, Enter. Password-only entries always lead with the password.
OpenVPN connections
The network mode shows an OpenVPN section when there's at least one
.ovpn/.conf config in the scan dir (default ~/.config/wlrun/vpn/,
override via [vpn] config_dir). Picking a row starts
sudo openvpn --daemon for that config.
The sudo password is captured in wlrun's own secret-input UI and piped
to sudo -S — no askpass window. A sudo -n true probe runs first, so a warm
credential cache skips the prompt. Configs with auth-user-pass also prompt
for VPN credentials, stored mode-0600 under $XDG_RUNTIME_DIR for the tunnel's
lifetime.
The connect flow is non-blocking: the launcher closes immediately and a
background goroutine watches for the "Initialization Sequence Completed"
marker, surfacing a "VPN connected" / "VPN connect failed" notification (with
the log tail on failure). Active tunnels disconnect via the
<name> — connected row or the explicit [Disconnect VPN: <name>] row.
[vpn]
config_dir = "~/.config/wlrun/vpn" # empty → the default above
Network mounts
Besides removable block devices and gvfs volumes, the mount mode lists
declarative network mounts read from a profiles file (default
~/.config/wlrun/mounts.toml, override via [mount] profiles_file). Each
[[profile]] becomes a row in a "Network mounts" section; picking it offers
Mount / Unmount, with the live state probed from /proc/mounts.
Four profile types are supported:
type |
mounts with | privilege | password |
|---|---|---|---|
sshfs |
sshfs (FUSE) |
your user | SSH key, or ask_password → -o password_stdin |
nfs |
sudo mount -t nfs |
sudo | — (sudo only) |
cifs |
sudo mount -t cifs |
sudo | ask_password → 0600 credentials file |
command |
your mount_command |
as written | — |
sudo passwords are captured in wlrun's own UI (same flow as OpenVPN, with the
sudo -n true warm-cache probe); sshfs/cifs passwords never reach the command
line. See mounts.sample.toml for the full format.
[mount]
profiles_file = "~/.config/wlrun/mounts.toml" # empty → the default above
# ~/.config/wlrun/mounts.toml
[[profile]]
name = "work-server"
type = "sshfs"
remote = "user@host:/srv/data"
mountpoint = "~/mnt/work"
options = ["reconnect"]
Contributing
Bug fixes, robustness improvements and new themes are very welcome. Before proposing new features, please read CONTRIBUTING.md — wlrun keeps a deliberately small, fixed feature set and will not take on GTK/Qt dependencies or grow into a generic scriptable launcher.
License
MIT — © 2026 Daniel Vigh.