diff options
| -rwxr-xr-x | config/hypr/scripts/KeyBinds.sh | 136 | ||||
| -rwxr-xr-x | config/hypr/scripts/keybinds_parser.py | 238 |
2 files changed, 248 insertions, 126 deletions
diff --git a/config/hypr/scripts/KeyBinds.sh b/config/hypr/scripts/KeyBinds.sh index 4158b762..26ae832b 100755 --- a/config/hypr/scripts/KeyBinds.sh +++ b/config/hypr/scripts/KeyBinds.sh @@ -21,135 +21,19 @@ msg='☣️ NOTE ☣️: Clicking with Mouse or Pressing ENTER will have NO func files=("$keybinds_conf" "$user_keybinds_conf") [[ -f "$laptop_conf" ]] && files+=("$laptop_conf") -# Parse binds/unbinds from files, detect overrides, and keep unique effective binds -declare -A binding_map # combo -> bind line (effective) -declare -A source_map # combo -> source file -declare -A user_bind_map # combo -> user bind line -declare -A unbound_user # combo -> 1 if explicitly unbound in user file -declare -A seen_any_bind # combo -> 1 if any bind seen (for iteration) -declare -A default_seen # combo -> 1 if default bind exists -declare -a missing_unbind_suggestions_arr +# Parse binds using the python script for speed +# The last argument must be the user config for override logic to work correctly +display_keybinds=$("$HOME/.config/hypr/scripts/keybinds_parser.py" "${files[@]}") -normalize_combo() { echo "$1" | sed -E 's/[[:space:]]//g'; } - -extract_combo() { - # arg: a bind/unbind line; returns "mods,key" via echo - local s="$1" - s="$(echo "$s" | sed -E 's/[[:space:]]+#.*$//')" - if [[ "$s" =~ = ]]; then - local rhs="${s#*=}" - local mods="$(echo "$rhs" | awk -F',' '{gsub(/^[ \t]+|[ \t]+$/,"",$1); print $1}')" - local key="$(echo "$rhs" | awk -F',' '{gsub(/^[ \t]+|[ \t]+$/,"",$2); print $2}')" - echo "${mods},${key}" - fi -} - -for file in "${files[@]}"; do - [[ ! -f "$file" ]] && continue - while IFS= read -r line; do - [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue - - if [[ "$line" =~ ^[[:space:]]*bind[a-z]*[[:space:]]*= ]]; then - combo_raw="$(extract_combo "$line")" - [[ -z "$combo_raw" ]] && continue - combo="$(normalize_combo "$combo_raw")" - seen_any_bind["$combo"]=1 - - if [[ "$file" != "$user_keybinds_conf" ]]; then - default_seen["$combo"]=1 - fi - - # prefer user bind, else first seen - if [[ -z "${source_map[$combo]}" ]]; then - binding_map["$combo"]="$line" - source_map["$combo"]="$file" - fi - if [[ "$file" == "$user_keybinds_conf" ]]; then - user_bind_map["$combo"]="$line" - binding_map["$combo"]="$line" - source_map["$combo"]="$file" - fi - - elif [[ "$line" =~ ^[[:space:]]*unbind[[:space:]]*= ]]; then - combo_raw="$(extract_combo "$line")" - [[ -z "$combo_raw" ]] && continue - combo="$(normalize_combo "$combo_raw")" - if [[ "$file" == "$user_keybinds_conf" ]]; then - unbound_user["$combo"]=1 - fi - fi - done < "$file" -done - -# Build raw_keybinds for display and collect missing unbind suggestions -raw_keybinds="" -for combo in "${!seen_any_bind[@]}"; do - eff_line="${binding_map[$combo]}" - src="${source_map[$combo]}" - [[ -z "$eff_line" ]] && continue - raw_keybinds+="$eff_line"$'\n' - - # If user overrides a default but didn't unbind in user file, suggest unbind - if [[ "$src" == "$user_keybinds_conf" && -n "${default_seen[$combo]}" && -z "${unbound_user[$combo]}" ]]; then - suggest="$(echo "$eff_line" | sed -E 's/^[[:space:]]*bind[a-z]*/unbind/')" - missing_unbind_suggestions_arr+=("$suggest") +# Check for suggestions file created by python script +if [[ -f "/tmp/hypr_keybind_suggestions_file" ]]; then + suggestions_file=$(cat "/tmp/hypr_keybind_suggestions_file") + rm "/tmp/hypr_keybind_suggestions_file" + if [[ -n "$suggestions_file" && -f "$suggestions_file" ]]; then + count=$(wc -l < "$suggestions_file") + msg="$msg | Overrides missing unbind: $count (suggestions: $suggestions_file)" fi -done - -# If there are missing unbinds, write suggestions to a temp file and note in message -if (( ${#missing_unbind_suggestions_arr[@]} > 0 )); then - suggestions_file="$(mktemp -t hypr-unbind-suggestions.XXXX.conf)" - printf '%s\n' "${missing_unbind_suggestions_arr[@]}" > "$suggestions_file" - msg="$msg | Overrides missing unbind: ${#missing_unbind_suggestions_arr[@]} (suggestions: $suggestions_file)" fi -# check for any keybinds to display -if [[ -z "$raw_keybinds" ]]; then - echo "no keybinds found." - exit 1 -fi - -# transform into a readable list: MODS+KEY — DESCRIPTION (for bindd) or DISPATCHER [PARAMS] (for bind) -display_keybinds=$(echo "$raw_keybinds" | awk -F'=' ' - function trim(s){ gsub(/^[ \t]+|[ \t]+$/,"",s); return s } - /^[[:space:]]*bind/ { - binder=$1; gsub(/[ \t]/, "", binder); - hasdesc = (index(binder, "d")>0); - - rhs=$2; rhs=trim(rhs); - n=split(rhs, a, /[ \t]*,[ \t]*/); - - mods=trim(a[1]); key=(n>=2?trim(a[2]):""); - desc=""; dispatcher=""; params=""; - - if (hasdesc) { - desc=(n>=3?trim(a[3]):""); - dispatcher=(n>=4?trim(a[4]):""); - start=5; - } else { - dispatcher=(n>=3?trim(a[3]):""); - start=4; - } - - for(i=start;i<=n;i++){ if(length(a[i])){ p=trim(a[i]); if(p!="") params = (params?params", ":"") p } } - - gsub(/\$mainMod/,"SUPER",mods); - gsub(/[ \t]+/,"+",mods); - - combo = (mods && key) ? mods "+" key : (key?key:mods); - - if (hasdesc && desc != "") { - print combo, " — ", desc; - } else { - if (dispatcher != "" && params != "") - print combo, " — ", dispatcher, " ", params; - else if (dispatcher != "") - print combo, " — ", dispatcher; - else - print combo; - } - } -') - # use rofi to display the keybinds printf '%s\n' "$display_keybinds" | rofi -dmenu -i -config "$rofi_theme" -mesg "$msg" diff --git a/config/hypr/scripts/keybinds_parser.py b/config/hypr/scripts/keybinds_parser.py new file mode 100755 index 00000000..cae57488 --- /dev/null +++ b/config/hypr/scripts/keybinds_parser.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python3 +import sys +import re +import os + +def normalize_combo(combo): + return combo.replace(" ", "").replace("\t", "") + +def extract_combo(line): + # Remove comments and whitespace + line = re.sub(r'\s*#.*$', '', line).strip() + + if '=' not in line: + return None + + try: + rhs = line.split('=', 1)[1] + parts = [p.strip() for p in rhs.split(',')] + if len(parts) < 2: + return None + + mods = parts[0] + key = parts[1] + return f"{mods},{key}" + except Exception: + return None + +def parse_files(files): + # Data structures to match original logic + binding_map = {} # combo -> effective line + source_map = {} # combo -> source file + user_bind_map = {} # combo -> user bind line + unbound_user = {} # combo -> True if explicitly unbound in user file + seen_any_bind = {} # combo -> True if seen + default_seen = {} # combo -> True if default bind exists + + # We assume the last file in the list is the user config (UserKeybinds.conf) + # This matches the bash script logic where user_keybinds_conf is passed last + if not files: + return [], [] + + user_conf_path = files[-1] if len(files) > 1 else None + + for file_path in files: + if not os.path.exists(file_path): + continue + + try: + with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: + for line in f: + line = line.rstrip('\n') + if not line or line.strip().startswith('#'): + continue + + is_bind = re.match(r'^\s*bind[a-z]*\s*=', line) + is_unbind = re.match(r'^\s*unbind\s*=', line) + + if is_bind: + combo_raw = extract_combo(line) + if not combo_raw: + continue + combo = normalize_combo(combo_raw) + seen_any_bind[combo] = True + + is_user_file = (file_path == user_conf_path) + + if not is_user_file: + default_seen[combo] = True + + # prefer user bind, else first seen + if combo not in source_map: + binding_map[combo] = line + source_map[combo] = file_path + + if is_user_file: + user_bind_map[combo] = line + binding_map[combo] = line + source_map[combo] = file_path + + elif is_unbind: + combo_raw = extract_combo(line) + if not combo_raw: + continue + combo = normalize_combo(combo_raw) + + if file_path == user_conf_path: + unbound_user[combo] = True + + except Exception as e: + # Silently ignore read errors to mimic bash behavior or log to stderr + sys.stderr.write(f"Error reading {file_path}: {e}\n") + continue + + # Build results + raw_keybinds = [] + missing_unbind_suggestions = [] + + for combo in seen_any_bind: + eff_line = binding_map.get(combo) + src = source_map.get(combo) + + if not eff_line: + continue + + raw_keybinds.append(eff_line) + + # Check for missing unbind suggestions + # If user overrides a default but didn't unbind in user file + if (src == user_conf_path and + combo in default_seen and + combo not in unbound_user): + + # Create suggestion: replace 'bind' with 'unbind' + suggest = re.sub(r'^\s*bind[a-z]*', 'unbind', eff_line) + missing_unbind_suggestions.append(suggest) + + return raw_keybinds, missing_unbind_suggestions + +def format_for_rofi(raw_binds): + formatted_lines = [] + + for line in raw_binds: + # line is like "bind = MODS, KEY, DISPATCHER, PARAMS" or "bindd = ..." + # Parsing logic from awk script: + + # 1. Cleaner binder + match = re.match(r'^\s*(bind[a-z]*)\s*=(.*)', line) + if not match: + continue + + binder = match.group(1).replace(" ", "").replace("\t", "") + rhs = match.group(2).strip() + + # "bind" ends in d, but doesn't have a description. "bindd" does. + # Original script logic `index(binder, "d")>0` was likely buggy for "bind". + # We'll assume strict check for bindd or similar if needed, + # but avoiding "bind" having a description is crucial for correct output. + has_desc = 'd' in binder and binder != 'bind' + + # Split by comma regex (handling spaces) + parts = [p.strip() for p in rhs.split(',')] + + if len(parts) < 2: + continue + + mods = parts[0] + key = parts[1] + + desc = "" + dispatcher = "" + params = "" + + start_idx = 0 + + if has_desc: + desc = parts[2] if len(parts) >= 3 else "" + dispatcher = parts[3] if len(parts) >= 4 else "" + start_idx = 4 + else: + dispatcher = parts[2] if len(parts) >= 3 else "" + start_idx = 3 + + # Collect params + remaining_parts = [] + if start_idx < len(parts): + for i in range(start_idx, len(parts)): + if parts[i]: + remaining_parts.append(parts[i]) + + if remaining_parts: + params = ", ".join(remaining_parts) + + # Formatting mods + mods = mods.replace("$mainMod", "SUPER") + mods = re.sub(r'[ \t]+', '+', mods) + + # Build combo string + if mods and key: + combo_str = f"{mods}+{key}" + elif key: + combo_str = key + else: + combo_str = mods + + # Final Print Format + if has_desc and desc: + formatted_lines.append(f"{combo_str} — {desc}") + elif dispatcher: + if params: + formatted_lines.append(f"{combo_str} — {dispatcher} {params}") + else: + formatted_lines.append(f"{combo_str} — {dispatcher}") + else: + formatted_lines.append(combo_str) + + return formatted_lines + +def main(): + if len(sys.argv) < 2: + # No files provided + sys.exit(0) + + config_files = sys.argv[1:] + + binds, suggestions = parse_files(config_files) + + if not binds: + print("no keybinds found.") + sys.exit(1) + + formatted = format_for_rofi(binds) + + for line in formatted: + print(line) + + # Handle suggestions (print to stderr or a specific file if needed, + # but the original script assigns it to a variable 'msg'. + # To pass this back to bash, we might need a separate mechanism or just print to a known file.) + if suggestions: + import tempfile + try: + with tempfile.NamedTemporaryFile(mode='w', delete=False, prefix='hypr-unbind-suggestions-', suffix='.conf') as tf: + tf.write('\n'.join(suggestions) + '\n') + # We print a special marker line to stdout that the bash script can capture? + # Or better, just print to stderr and let the user ignore it, + # OR, since the original script specifically puts it in the Rofi message, + # we can print a special string at the END of stdout or to a side channel. + + # Let's decide to print the valid keybinds to stdout (for rofi). + # And print the suggestion file path to a known location or specific fd if possible. + # Simplest: Write to a fixed temp file location that the bash script checks. + with open("/tmp/hypr_keybind_suggestions_file", "w") as sf: + sf.write(tf.name) + except Exception: + pass + +if __name__ == "__main__": + main() |
