#!/bin/bash # omarchy:summary=Display annotated Herdr keybindings using an interactive search menu. # omarchy:args=[--print|-p] [--config ] 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