aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xconfig/hypr/scripts/KeyBinds.sh136
-rwxr-xr-xconfig/hypr/scripts/keybinds_parser.py238
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()
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage