#!/bin/bash # Display Hyprland keybindings defined in your configuration using walker for an interactive search menu. declare -A KEYCODE_SYM_MAP build_keymap_cache() { local keymap keymap="$(xkbcli compile-keymap)" || { echo "Failed to compile keymap" >&2 return 1 } while IFS=, read -r code sym; do [[ -z "$code" || -z "$sym" ]] && continue KEYCODE_SYM_MAP["$code"]="$sym" done < <( awk ' BEGIN { sec = "" } /xkb_keycodes/ { sec = "codes"; next } /xkb_symbols/ { sec = "syms"; next } sec == "codes" { if (match($0, /<([A-Za-z0-9_]+)>\s*=\s*([0-9]+)\s*;/, m)) code_by_name[m[1]] = m[2] } sec == "syms" { if (match($0, /key\s*<([A-Za-z0-9_]+)>\s*\{\s*\[\s*([^, \]]+)/, m)) sym_by_name[m[1]] = m[2] } END { for (k in code_by_name) { c = code_by_name[k] s = sym_by_name[k] if (c != "" && s != "" && s != "NoSymbol") print c "," s } } ' <<<"$keymap" ) } lookup_keycode_cached() { printf '%s\n' "${KEYCODE_SYM_MAP[$1]}" } parse_keycodes() { local start end elapsed [[ "${DEBUG:-0}" == "1" ]] && start=$(date +%s.%N) while IFS= read -r line; do if [[ "$line" =~ code:([0-9]+) ]]; then code="${BASH_REMATCH[1]}" symbol=$(lookup_keycode_cached "$code" "$XKB_KEYMAP_CACHE") echo "${line/code:${code}/$symbol}" elif [[ "$line" =~ mouse:([0-9]+) ]]; then code="${BASH_REMATCH[1]}" case "$code" in 272) symbol="LEFT MOUSE BUTTON" ;; 273) symbol="RIGHT MOUSE BUTTON" ;; 274) symbol="MIDDLE MOUSE BUTTON" ;; *) symbol="mouse:${code}" ;; esac echo "${line/mouse:${code}/$symbol}" else echo "$line" fi done if [[ "$DEBUG" == "1" ]]; then end=$(date +%s.%N) # fall back to awk if bc is missing if command -v bc >/dev/null 2>&1; then elapsed=$(echo "$end - $start" | bc) else elapsed=$(awk -v s="$start" -v e="$end" 'BEGIN{printf "%.6f", (e - s)}') fi echo "[DEBUG] parse_keycodes elapsed: ${elapsed}s" >&2 fi } # Fetch dynamic keybindings from Hyprland # # Also do some pre-processing: # - Remove standard Omarchy bin path prefix # - Remove uwsm prefix # - Map numeric modifier key mask to a textual rendition # - Output comma-separated values that the parser can understand dynamic_bindings() { hyprctl -j binds | jq -r '.[] | {modmask, key, keycode, description, dispatcher, arg} | "\(.modmask),\(.key)@\(.keycode),\(.description),\(.dispatcher),\(.arg)"' | sed -r \ -e 's/null//' \ -e 's,~/.local/share/omarchy/bin/,,' \ -e 's,uwsm app -- ,,' \ -e 's,uwsm-app -- ,,' \ -e 's/@0//' \ -e 's/,@/,code:/' \ -e 's/^0,/,/' \ -e 's/^1,/SHIFT,/' \ -e 's/^4,/CTRL,/' \ -e 's/^5,/SHIFT CTRL,/' \ -e 's/^8,/ALT,/' \ -e 's/^9,/SHIFT ALT,/' \ -e 's/^12,/CTRL ALT,/' \ -e 's/^13,/SHIFT CTRL ALT,/' \ -e 's/^64,/SUPER,/' \ -e 's/^65,/SUPER SHIFT,/' \ -e 's/^68,/SUPER CTRL,/' \ -e 's/^69,/SUPER SHIFT CTRL,/' \ -e 's/^72,/SUPER ALT,/' \ -e 's/^73,/SUPER SHIFT ALT,/' \ -e 's/^76,/SUPER CTRL ALT,/' \ -e 's/^77,/SUPER SHIFT CTRL ALT,/' } # Hardcoded bindings, like the copy-url extension and such static_bindings() { echo "SHIFT ALT,L,Copy URL from Web App,extension,copy-url" } # Parse and format keybindings # # `awk` does the heavy lifting: # - Set the field separator to a comma ','. # - Joins the key combination (e.g., "SUPER + Q"). # - Joins the command that the key executes. # - Prints everything in a nicely aligned format. parse_bindings() { awk -F, ' { # Combine the modifier and key (first two fields) key_combo = $1 " + " $2; # Clean up: strip leading "+" if present, trim spaces gsub(/^[ \t]*\+?[ \t]*/, "", key_combo); gsub(/[ \t]+$/, "", key_combo); # Use description, if set action = $3; if (action == "") { # Reconstruct the command from the remaining fields for (i = 4; i <= NF; i++) { action = action $i (i < NF ? "," : ""); } # Clean up trailing commas, remove leading "exec, ", and trim sub(/,$/, "", action); gsub(/(^|,)[[:space:]]*exec[[:space:]]*,?/, "", action); gsub(/^[ \t]+|[ \t]+$/, "", action); gsub(/[ \t]+/, " ", key_combo); # Collapse multiple spaces to one # Escape XML entities gsub(/&/, "\\&", action); gsub(//, "\\>", action); gsub(/"/, "\\"", action); gsub(/'"'"'/, "\\'", action); } if (action != "") { printf "%-35s → %s\n", key_combo, action; } }' } prioritize_entries() { awk ' { line = $0 prio = 50 if (match(line, /Terminal/)) prio = 0 if (match(line, /Browser/) && !match(line, /Browser[[:space:]]*\(/)) prio = 1 if (match(line, /File manager/)) prio = 2 if (match(line, /Launch apps/)) prio = 3 if (match(line, /Omarchy menu/)) prio = 4 if (match(line, /System menu/)) prio = 5 if (match(line, /Theme menu/)) prio = 6 if (match(line, /Full screen/)) prio = 7 if (match(line, /Full width/)) prio = 8 if (match(line, /Close window/)) prio = 9 if (match(line, /Close all windows/)) prio = 10 if (match(line, /Lock system/)) prio = 11 if (match(line, /Toggle window floating/)) prio = 12 if (match(line, /Toggle window split/)) prio = 13 if (match(line, /Pop window/)) prio = 14 if (match(line, /Universal/)) prio = 15 if (match(line, /Clipboard/)) prio = 16 if (match(line, /Audio controls/)) prio = 17 if (match(line, /Bluetooth controls/)) prio = 18 if (match(line, /Wifi controls/)) prio = 19 if (match(line, /Emoji picker/)) prio = 20 if (match(line, /Color picker/)) prio = 21 if (match(line, /Screenshot/)) prio = 22 if (match(line, /Screenrecording/)) prio = 23 if (match(line, /(Switch|Next|Former|Previous).*workspace/)) prio = 24 if (match(line, /Move window to workspace/)) prio = 25 if (match(line, /Move window silently to workspace/)) prio = 26 if (match(line, /Swap window/)) prio = 27 if (match(line, /Move window focus/)) prio = 28 if (match(line, /Move window$/)) prio = 29 if (match(line, /Resize window/)) prio = 30 if (match(line, /Expand window/)) prio = 31 if (match(line, /Shrink window/)) prio = 32 if (match(line, /scratchpad/)) prio = 33 if (match(line, /notification/)) prio = 34 if (match(line, /Toggle window transparency/)) prio = 35 if (match(line, /Toggle workspace gaps/)) prio = 36 if (match(line, /Toggle nightlight/)) prio = 37 if (match(line, /Toggle locking/)) prio = 38 if (match(line, /group/)) prio = 94 if (match(line, /Scroll active workspace/)) prio = 95 if (match(line, /Cycle to/)) prio = 96 if (match(line, /Reveal active/)) prio = 97 if (match(line, /Apple Display/)) prio = 98 if (match(line, /XF86/)) prio = 99 # print "priorityline" printf "%d\t%s\n", prio, line }' | sort -k1,1n -k2,2 | cut -f2- } output_keybindings() { build_keymap_cache { dynamic_bindings static_bindings } | sort -u | parse_keycodes | parse_bindings | prioritize_entries } if [[ "$1" == "--print" || "$1" == "-p" ]]; then output_keybindings else monitor_height=$(hyprctl monitors -j | jq -r '.[] | select(.focused == true) | .height') menu_height=$((monitor_height * 40 / 100)) output_keybindings | walker --dmenu -p 'Keybindings' --width 800 --height "$menu_height" fi