Repository navigation
Plugins
A plugin is any executable file in ~/.config/zunder/plugins/: a shell script, Python, a
compiled binary, anything. zunder finds it when it opens, so there's nothing to register.
A few comments near the top (first 30 lines) configure it. All of them are optional.
#!/bin/sh
# @zunder.name Kill Process
# @zunder.prefix kill
# @zunder.glyph
# @zunder.mode search| Key | Default | What it does |
|---|---|---|
name |
file name | shown in search and as the section title |
prefix |
file name | typing <prefix> (with the space) hands the query to the plugin |
glyph |
puzzle piece | Nerd Font icon, the character itself or \uXXXX (cheat sheet) |
mode |
search |
search or command
|
Typing <prefix> <query> runs script "<query>" on every keystroke, in a background thread.
Results from an older keystroke that arrive late are thrown away, so a slow plugin never shows
stale results.
Every line on stdout is one result, in order.
Plain text: Enter copies the line. That makes almost anything a plugin:
#!/bin/sh
# @zunder.prefix note
# @zunder.glyph
grep -ri -- "$1" ~/notes | head -50JSON: one object per line for full control:
{"title": "zen-bin", "subtitle": "pid 4242", "hint": "6% · 712 MB", "glyph": "", "action": {"run": "kill 4242"}}| Field | Required | What it is |
|---|---|---|
title |
yes | main text |
subtitle |
no | dim text after the title |
hint |
no | right-aligned text |
glyph |
no | icon for this line, defaults to the plugin's |
icon |
no | icon theme name or absolute path, e.g. firefox
|
image |
no | path to an image, shown in the preview pane when the line is selected |
preview |
no | text shown in the preview pane, e.g. a whole note or a command's output |
action |
no | what Enter does, see below. Without one, Enter does nothing |
| Action | Enter does |
|---|---|
{"run": "cmd"} |
runs cmd with sh -c in the background |
{"term": "cmd"} |
runs cmd in your terminal, which stays open after it exits |
{"open": "url or path"} |
xdg-open |
{"copy": "text"} |
copies to the clipboard |
{"focus": "0x55a3…"} |
focuses a Hyprland window by address |
{"fill": "text"} |
replaces the query and keeps zunder open, for menus and sub-pages |
Results get an id from the plugin name and title, so they can be favorites (Ctrl+F) and get global shortcuts (Ctrl+B) like apps.
If the script exits non-zero without printing anything, the last line of its stderr is shown
as the result, so echo "need an API key" >&2; exit 1 gives a readable error.
#!/bin/sh
# @zunder.name Toggle VPN
# @zunder.mode command
# @zunder.glyph
if nmcli -t connection show --active | grep -q wg0; then nmcli connection down wg0; else nmcli connection up wg0; fiCommand plugins show up in normal search under their name. Enter runs the script once, with no arguments, detached.
In examples/plugins:
-
kill: search your processes by name, sorted by CPU, Enter kills one -
ip: command plugin, copies your local IP
mkdir -p ~/.config/zunder/plugins
ln -s "$PWD"/examples/plugins/* ~/.config/zunder/plugins/
zunder reloadA few more ideas, each a handful of lines:
GitHub repos (needs gh):
#!/bin/sh
# @zunder.name GitHub
# @zunder.prefix gh
# @zunder.glyph
gh repo list --limit 100 --json nameWithOwner,description,url \
--jq ".[] | select(.nameWithOwner | test(\"$1\"; \"i\")) |
{title: .nameWithOwner, subtitle: (.description // \"\"), action: {open: .url}}" -cQuicklinks with a query:
#!/usr/bin/env python3
# @zunder.name Quicklinks
# @zunder.prefix q
# @zunder.glyph
import json, sys, urllib.parse
q = sys.argv[1] if len(sys.argv) > 1 else ""
links = {
"yt": ("YouTube", "https://www.youtube.com/results?search_query={}"),
"wiki": ("Wikipedia", "https://en.wikipedia.org/w/index.php?search={}"),
"aur": ("AUR", "https://aur.archlinux.org/packages?K={}"),
"crates": ("crates.io", "https://crates.io/search?q={}"),
}
key, _, rest = q.partition(" ")
for k, (name, url) in links.items():
if k.startswith(key):
print(json.dumps({"title": f"{name}: {rest}" if rest else name, "hint": k,
"action": {"open": url.format(urllib.parse.quote(rest))} if rest
else {"fill": f"q {k} "}}))A menu inside a plugin with fill: the first level prints {"title": "Wi-Fi", "action": {"fill": "net wifi "}}, and the script handles wifi … on the next call.
hl.bind("SUPER + X", hl.dsp.exec_cmd("zunder show 'kill '"))Or select the plugin in zunder and press Ctrl+B.
- Keep it fast: it runs on every keystroke. Cache slow lookups in a file under
/tmp. - Print at most a few hundred lines; zunder doesn't truncate plugin output.
-
zunder reloadpicks up new plugins and header changes immediately. Script changes don't need it, the file runs fresh every time.