#!/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
