1
0
Fork
You've already forked wlrun
0
Keyboard-driven launcher and system console for wlr-layer-shell compositors: apps, network, Bluetooth, display, storage, keyboard, pass
  • Go 99.1%
  • Shell 0.9%
Vígh Dániel cfb83c26c3
Some checks failed
ci / lint (push) Has been cancelled
ci / test (push) Has been cancelled
build(aur): wlrun 1.11.0 sha256
2026年07月07日 11:07:13 +02:00
.forgejo ci: add wlbar-style test + lint workflow 2026年06月19日 10:45:24 +02:00
assets Document new theming + compositor-agnostic keyboard, add theme screenshots 2026年05月29日 13:16:10 +02:00
cmd/wlrun feat(mount): network mount profiles + cancellable async actions 2026年06月29日 12:04:37 +02:00
examples rename: river-delta → wlrun across code, docs and packaging 2026年06月05日 14:36:48 +02:00
internal feat(pass): cache GPG passphrase + lead sub-menu with next action 2026年07月07日 10:48:50 +02:00
packaging/aur build(aur): wlrun 1.11.0 sha256 2026年07月07日 11:07:13 +02:00
protocols Initial commit: dmenu-style Wayland launcher for river 2026年05月23日 18:01:27 +02:00
.gitignore rename: river-delta → wlrun across code, docs and packaging 2026年06月05日 14:36:48 +02:00
.golangci.yml lint: migrate golangci-lint to v2 schema and fix findings 2026年06月19日 10:45:05 +02:00
config.sample.toml feat(pass): cache GPG passphrase + lead sub-menu with next action 2026年07月07日 10:48:50 +02:00
CONTRIBUTING.md docs: document fixed-scope, no-GTK/Qt project boundaries 2026年06月08日 10:31:53 +02:00
go.mod rename: river-delta → wlrun across code, docs and packaging 2026年06月05日 14:36:48 +02:00
go.sum Rasterise glyphs via cgo FreeType for crisp hinted text 2026年05月28日 08:56:34 +02:00
LICENSE Initial commit: dmenu-style Wayland launcher for river 2026年05月23日 18:01:27 +02:00
mounts.sample.toml feat(mount): network mount profiles + cancellable async actions 2026年06月29日 12:04:37 +02:00
README.md feat(pass): cache GPG passphrase + lead sub-menu with next action 2026年07月07日 10:48:50 +02:00

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) and freetype2 (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.