#!/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(/&/, "\\&amp;", action);
        gsub(/</, "\\&lt;", action);
        gsub(/>/, "\\&gt;", action);
        gsub(/"/, "\\&quot;", action);
        gsub(/'"'"'/, "\\&apos;", 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 "priority<TAB>line"
    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

