Files
omarchycn/bin/omarchy-menu-herdr-keybindings
T
David Heinemeier Hansson 03b825f59a Show Herdr keybindings with SUPER + CTRL + K
Mirrors the Tmux keybindings menu on SUPER + ALT + K. Herdr has no CLI
that dumps resolved bindings, so the action list and its defaults come
from `herdr --default-config`, where every action appears as a commented
assignment, and the user config overrides what it sets.

Prose in that default config can read like an assignment, as in
`# type = "popup" opens a session-modal terminal`, so a line counts only
when its value is a bare string or an array of them. Both TOML quote
characters open a string, and with no config file at all the listing
shows Herdr's own defaults rather than the Omarchy seed config Herdr
never loaded.
2026-08-10 18:44:19 +02:00

232 lines
5.7 KiB
Bash
Executable File

#!/bin/bash
# omarchy:summary=Display annotated Herdr keybindings using an interactive search menu.
# omarchy:args=[--print|-p] [--config <path>]
print_only=false
config_file="${HERDR_CONFIG_PATH:-${XDG_CONFIG_HOME:-$HOME/.config}/herdr/config.toml}"
while (($#)); do
case "$1" in
--print|-p)
print_only=true
;;
--config)
shift
config_file="$1"
;;
*)
config_file="$1"
;;
esac
shift
done
# With no config file, Herdr runs on its own built-in defaults. Substituting the
# Omarchy seed config here would describe bindings Herdr never loaded.
[[ -f $config_file ]] || config_file="/dev/null"
# Herdr has no CLI that dumps resolved keybindings, so the action list and its
# defaults come from `herdr --default-config`, where every action appears as a
# commented `# action = "binding"` line, and the user config overrides those.
output_keybindings() (
set -o pipefail
herdr --default-config | awk -v config_file="$config_file" '
# TOML literal strings are as valid as basic ones, so both quote characters
# open and close a value here.
function strip_comment(value, i, ch, quote, out) {
quote = ""
out = ""
for (i = 1; i <= length(value); i++) {
ch = substr(value, i, 1)
if (quote == "" && (ch == "\"" || ch == "'\''")) quote = ch
else if (ch == quote) quote = ""
if (ch == "#" && quote == "") break
out = out ch
}
return out
}
# Removes every quoted run, leaving only what surrounds the strings. An
# unterminated quote returns a sentinel no valid value can produce.
function without_quoted(value, i, ch, quote, out) {
quote = ""
out = ""
for (i = 1; i <= length(value); i++) {
ch = substr(value, i, 1)
if (quote == "") {
if (ch == "\"" || ch == "'"'"'") quote = ch
else out = out ch
} else if (ch == quote) {
quote = ""
}
}
return quote == "" ? out : "\001"
}
# Prose in the default config can read like an assignment, as in
# `# type = "popup" opens a session-modal terminal`, so a value counts only when
# it is a bare string or an array of them.
function is_binding_value(value, rest) {
value = strip_comment(value)
sub(/^[[:space:]]+/, "", value)
sub(/[[:space:]]+$/, "", value)
if (value == "") return 0
rest = without_quoted(value)
# A bare string leaves nothing behind; an array leaves only its brackets,
# commas, and whitespace.
return rest == "" || rest ~ /^\[[[:space:],]*\]$/
}
function key_text(key, count, parts, i, text) {
count = split(key, parts, "+")
text = ""
for (i = 1; i <= count; i++) {
text = text (text == "" ? "" : " + ") toupper(parts[i])
}
return text
}
# Renders every alternate binding for one action as "A / B".
function combo_text(value, i, ch, quote, part, text) {
value = strip_comment(value)
quote = ""
part = ""
text = ""
for (i = 1; i <= length(value); i++) {
ch = substr(value, i, 1)
if (quote == "") {
if (ch == "\"" || ch == "'"'"'") { quote = ch; part = "" }
continue
}
if (ch == quote) {
quote = ""
if (part != "") text = text (text == "" ? "" : " / ") key_text(part)
continue
}
part = part ch
}
return text
}
function describe(action, text) {
sub(/^navigate_/, "", action)
gsub(/_/, " ", action)
text = toupper(substr(action, 1, 1)) substr(action, 2)
return text
}
function remember(action) {
if (action in seen) return
seen[action] = 1
order[++count] = action
}
function bind(action, value, combo) {
combo = combo_text(value)
if (combo == "") return
if (action ~ /^navigate_/) combo = "NAVIGATE + " combo
remember(action)
combos[action] = combo
}
# Pass 1: default config on stdin, for the action order and default bindings.
# Every line there is commented out, so section markers are too.
FNR == NR {
line = $0
sub(/^[[:space:]]*#[[:space:]]*/, "", line)
if (line ~ /^\[\[keys\.command\]\]/) { in_keys = 0; next }
if (line ~ /^\[/) { in_keys = (line ~ /^\[keys\]/); next }
if (!in_keys) next
if (line !~ /^[a-z_]+[[:space:]]*=/) next
action = line
sub(/[[:space:]]*=.*$/, "", action)
value = line
sub(/^[^=]*=[[:space:]]*/, "", value)
if (!is_binding_value(value)) next
# Actions Herdr leaves unbound still hold their place in the listing order,
# for when the user config binds them.
remember(action)
bind(action, value)
next
}
# Pass 2: the user config, which overrides the defaults it sets.
{
if ($0 ~ /^\[\[keys\.command\]\]/) { in_user_keys = 0; next }
if ($0 ~ /^\[/) { in_user_keys = ($0 ~ /^\[keys\]/); next }
if (!in_user_keys) next
if ($0 ~ /^[[:space:]]*#/) next
if ($0 !~ /^[[:space:]]*[a-z_]+[[:space:]]*=/) next
line = $0
sub(/^[[:space:]]*/, "", line)
action = line
sub(/[[:space:]]*=.*$/, "", action)
value = line
sub(/^[^=]*=[[:space:]]*/, "", value)
if (!is_binding_value(value)) next
# An action bound to nothing in the user config is unbound, not defaulted.
if (combo_text(value) == "") {
delete combos[action]
next
}
bind(action, value)
}
END {
if ("prefix" in combos) {
printf "%-32s → %s\n", "PREFIX", combos["prefix"]
}
for (i = 1; i <= count; i++) {
action = order[i]
if (action == "prefix") continue
if (!(action in combos)) continue
printf "%-32s → %s\n", combos[action], describe(action)
}
}
' - "$config_file"
)
if [[ $print_only == "true" ]]; then
output_keybindings
exit 0
fi
records=$(output_keybindings)
monitor_height=$(hyprctl monitors -j 2>/dev/null | jq -r '.[] | select(.focused == true) | .height' 2>/dev/null)
if [[ ! $monitor_height =~ ^[0-9]+$ ]] || ((monitor_height <= 0)); then
monitor_height=900
fi
menu_height=$((monitor_height * 40 / 100))
printf '%s\n' "$records" | omarchy-menu-select 'Herdr keybindings' -- --width 800 --height "$menu_height" >/dev/null || true