dotfiles/.config/hypr/scripts/keybinds_parser.py
2026-05-13 21:22:17 +02:00

251 lines
8.9 KiB
Python
Executable file

#!/usr/bin/env python3
# ==================================================
# KoolDots (2026)
# Project URL: https://github.com/LinuxBeginnings
# License: GNU GPLv3
# SPDX-License-Identifier: GPL-3.0-or-later
# ==================================================
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
# If unbind is found, we should remove the bind from our map
# so it doesn't show up in the menu.
if combo in binding_map:
del binding_map[combo]
if combo in source_map:
del source_map[combo]
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()