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.
232 lines
5.7 KiB
Bash
Executable File
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
|