#!/bin/bash # omarchy:summary=Display Hyprland keybindings defined in your configuration using walker for an interactive search menu. declare -A KEYCODE_SYM_MAP # Hyprland reports XKB keycodes for code: bindings. Keep a small fallback for # common keys so the menu remains readable if xkbcli cannot resolve a symbol. declare -A FALLBACK_KEYCODE_SYM_MAP=( [10]="1" [11]="2" [12]="3" [13]="4" [14]="5" [15]="6" [16]="7" [17]="8" [18]="9" [19]="0" [20]="MINUS" [21]="EQUAL" [61]="SLASH" ) # Hyprland's Lua config provider currently reports code:... binds from hl.bind() # as key="" and keycode=0 in `hyprctl -j binds`. Keep a lightweight # source-derived key cache so the menu can still show those bindings. declare -A LUA_BIND_KEY_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() { local symbol symbol="${KEYCODE_SYM_MAP[$1]}" if [[ -z $symbol ]]; then symbol="${FALLBACK_KEYCODE_SYM_MAP[$1]}" fi if [[ -z $symbol ]]; then symbol="code:$1" fi printf '%s\n' "${symbol^^}" } 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]}" if [[ $code == "201" ]]; then echo "${line/SUPER SHIFT,code:201/,COPILOT KEY}" else symbol=$(lookup_keycode_cached "$code" "$XKB_KEYMAP_CACHE") echo "${line/code:${code}/$symbol}" fi 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 } # Supplement `hyprctl -j binds` for Lua-only binds that Hyprland currently # reports without their original key, such as hl.bind("SUPER + code:10", ...). build_lua_bind_key_cache() { local modmask description key omarchy-cmd-present lua || return 0 while IFS=$'\t' read -r modmask description key; do [[ -z $modmask || -z $description || -z $key ]] && continue LUA_BIND_KEY_MAP["$modmask,$description"]="$key" done < <( lua <<'LUA' local modifiers = { SHIFT = 1, CTRL = 4, CONTROL = 4, ALT = 8, SUPER = 64 } local function split_keys(keys) local modmask = 0 local key = "" for part in string.gmatch(keys, "[^+]+") do local value = part:gsub("^%s+", ""):gsub("%s+$", "") local modifier = modifiers[string.upper(value)] if modifier then modmask = modmask + modifier else key = value end end return modmask, key end local function proxy() local p = {} return setmetatable(p, { __index = function() return p end, __call = function() return p end, }) end local p = proxy() hl = setmetatable({ dsp = p, bind = function(keys, _, opts) opts = opts or {} if opts.description and opts.description ~= "" then local modmask, key = split_keys(keys) print(modmask .. "\t" .. opts.description .. "\t" .. key) end return p end, unbind = function() end, config = function() end, env = function() end, monitor = function() end, window_rule = function() end, gesture = function() end, animation = function() end, curve = function() end, exec_cmd = function() end, }, { __index = function() return function() return p end end, }) local config = (os.getenv("HOME") or "") .. "/.config/hypr/hyprland.lua" local file = io.open(config, "r") if file then file:close() local ok, err = pcall(dofile, config) if not ok and os.getenv("DEBUG") == "1" then io.stderr:write("[DEBUG] lua bind scan failed: " .. tostring(err) .. "\n") end end LUA ) } modmask_to_text() { case "$1" in 0) printf '' ;; 1) printf 'SHIFT' ;; 4) printf 'CTRL' ;; 5) printf 'SHIFT CTRL' ;; 8) printf 'ALT' ;; 9) printf 'SHIFT ALT' ;; 12) printf 'CTRL ALT' ;; 13) printf 'SHIFT CTRL ALT' ;; 64) printf 'SUPER' ;; 65) printf 'SUPER SHIFT' ;; 68) printf 'SUPER CTRL' ;; 69) printf 'SUPER SHIFT CTRL' ;; 72) printf 'SUPER ALT' ;; 73) printf 'SUPER SHIFT ALT' ;; 76) printf 'SUPER CTRL ALT' ;; 77) printf 'SUPER SHIFT CTRL ALT' ;; *) printf '%s' "$1" ;; esac } # Fetch dynamic keybindings from Hyprland. # # Also do some pre-processing: # - Fill missing Lua code:... keys from the Lua source cache # - Remove standard Omarchy bin path prefix # - Map numeric modifier key mask to a textual rendition # - Output comma-separated values that the parser can understand dynamic_bindings() { local modmask key keycode description dispatcher arg modifiers hyprctl -j binds | jq -r '.[] | [.modmask, (.key // ""), (.keycode // 0), (.description // ""), (.dispatcher // ""), (.arg // "") | tostring] | join("\u001f")' | while IFS=$'\x1f' read -r modmask key keycode description dispatcher arg; do if [[ -z $key && $keycode != "0" ]]; then key="code:$keycode" fi if [[ -z $key && -n $description ]]; then key="${LUA_BIND_KEY_MAP["$modmask,$description"]}" fi [[ -z $description && $dispatcher == "__lua" ]] && continue modifiers=$(modmask_to_text "$modmask") arg="${arg//~\/.local\/share\/omarchy\/bin\//}" arg="${arg//uwsm app -- /}" arg="${arg//uwsm-app -- /}" printf '%s,%s,%s,%s,%s\n' "$modifiers" "$key" "$description" "$dispatcher" "$arg" done } # Hardcoded bindings, like the copy-url extension and such static_bindings() { echo "SHIFT ALT,L,Copy URL from Web App,sendshortcut,SHIFT ALT,L," } # 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 display text and dispatch metadata as tab-separated fields. parse_binding_records() { 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; dispatcher = $4; # Reconstruct the dispatcher arg from the remaining fields arg = ""; for (i = 5; i <= NF; i++) { arg = arg $i (i < NF ? "," : ""); } 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(/(^|[[:space:]])uwsm(-app| app)[[:space:]]+--[[: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\t%s\t%s\n", key_combo, action, dispatcher, arg; } }' } prioritize_entries() { awk -F '\t' ' { line = $1 prio = 50 if (match(line, /Terminal/)) prio = 0 if (match(line, /Tmux/)) prio = 1 if (match(line, /Browser/) && !match(line, /Browser[[:space:]]*\(/) && !match(line, /SUPER SHIFT.*\+.*B.*→.*Browser/)) prio = 2 if (match(line, /File manager/) && !match(line, /File manager \(cwd\)/)) prio = 3 if (match(line, /Launch apps/)) prio = 4 if (match(line, /Omarchy menu/)) prio = 5 if (match(line, /System menu/)) prio = 6 if (match(line, /Theme menu/)) prio = 7 if (match(line, /Full screen/)) prio = 8 if (match(line, /Full width/)) prio = 9 if (match(line, /Close window/)) prio = 10 if (match(line, /Close all windows/)) prio = 11 if (match(line, /Lock system/)) prio = 12 if (match(line, /Toggle window floating/)) prio = 13 if (match(line, /Toggle window split/)) prio = 14 if (match(line, /Pop window/)) prio = 15 if (match(line, /Universal/)) prio = 16 if (match(line, /Clipboard/)) prio = 17 if (match(line, /Audio controls/)) prio = 18 if (match(line, /Bluetooth controls/)) prio = 19 if (match(line, /Wifi controls/)) prio = 20 if (match(line, /Emoji picker/)) prio = 21 if (match(line, /Color picker/)) prio = 22 if (match(line, /Screenshot/)) prio = 23 if (match(line, /Screenrecording/)) prio = 24 if (match(line, /SUPER SHIFT.*\+.*B.*→.*Browser/)) prio = 25 if (match(line, /File manager \(cwd\)/)) prio = 26 if (match(line, /(Switch|Next|Former|Previous).*workspace/)) prio = 27 if (match(line, /Move window to workspace/)) prio = 28 if (match(line, /Move window silently to workspace/)) prio = 29 if (match(line, /Swap window/)) prio = 30 if (match(line, /Focus/)) prio = 31 if (match(line, /Move window$/)) prio = 32 if (match(line, /Resize window/)) prio = 33 if (match(line, /Expand window/)) prio = 34 if (match(line, /Shrink window/)) prio = 35 if (match(line, /scratchpad/)) prio = 36 if (match(line, /notification/)) prio = 37 if (match(line, /Toggle window transparency/)) prio = 38 if (match(line, /Toggle workspace gaps/)) prio = 39 if (match(line, /Toggle nightlight/)) prio = 40 if (match(line, /Toggle locking/)) prio = 41 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 "priorityrecord" printf "%d\t%s\n", prio, $0 }' | sort -k1,1n -k2,2 | cut -f2- } output_binding_records() { build_keymap_cache build_lua_bind_key_cache { dynamic_bindings static_bindings } | sort -u | parse_keycodes | parse_binding_records | prioritize_entries } output_keybindings() { output_binding_records | cut -f1 } dispatch_binding() { local dispatcher="$1" local arg="$2" case "$dispatcher" in exec) [[ -n $arg ]] && hyprctl dispatch exec "$arg" ;; "") return 1 ;; *) if [[ -n $arg ]]; then hyprctl dispatch "$dispatcher" "$arg" else hyprctl dispatch "$dispatcher" fi ;; esac } if [[ $1 == "--print" || $1 == "-p" ]]; then output_keybindings else records=$(output_binding_records) monitor_height=$(hyprctl monitors -j | jq -r '.[] | select(.focused == true) | .height') menu_height=$((monitor_height * 40 / 100)) selection=$(cut -f1 <<<"$records" | walker --dmenu -p 'Keybindings' --width 800 --height "$menu_height") if [[ -n $selection ]]; then record=$(awk -F '\t' -v selection="$selection" '$1 == selection { print; exit }' <<<"$records") dispatcher=$(cut -f2 <<<"$record") arg=$(cut -f3- <<<"$record") dispatch_binding "$dispatcher" "$arg" fi fi