diff options
Diffstat (limited to 'config/hypr/scripts/keybinds_parser.py')
| -rwxr-xr-x | config/hypr/scripts/keybinds_parser.py | 270 |
1 files changed, 269 insertions, 1 deletions
diff --git a/config/hypr/scripts/keybinds_parser.py b/config/hypr/scripts/keybinds_parser.py index 5b75d81c..bd6142d9 100755 --- a/config/hypr/scripts/keybinds_parser.py +++ b/config/hypr/scripts/keybinds_parser.py @@ -8,10 +8,30 @@ import sys import re import os +CODE_KEY_MAP = { + 10: "1", + 11: "2", + 12: "3", + 13: "4", + 14: "5", + 15: "6", + 16: "7", + 17: "8", + 18: "9", + 19: "0", +} def normalize_combo(combo): return combo.replace(" ", "").replace("\t", "") +def humanize_key_token(mods, key): + key = key.strip() + code_match = re.match(r'(?i)^code:(\d+)$', key) + if code_match: + code_num = int(code_match.group(1)) + return CODE_KEY_MAP.get(code_num, key) + return key + def extract_combo(line): # Remove comments and whitespace line = re.sub(r'\s*#.*$', '', line).strip() @@ -128,6 +148,244 @@ def parse_files(files): missing_unbind_suggestions.append(suggest) return raw_keybinds, missing_unbind_suggestions +def _parse_lua_string(value): + value = value.strip() + if len(value) < 2: + return None + if (value[0] == value[-1]) and value[0] in ("'", '"'): + quote = value[0] + body = value[1:-1] + if quote == '"': + body = body.replace('\\"', '"') + else: + body = body.replace("\\'", "'") + body = body.replace("\\\\", "\\") + return body + return None + +def _split_lua_args(arg_text): + args = [] + current = [] + depth = 0 + in_string = None + escape = False + for ch in arg_text: + if in_string: + current.append(ch) + if escape: + escape = False + elif ch == '\\': + escape = True + elif ch == in_string: + in_string = None + continue + if ch in ("'", '"'): + in_string = ch + current.append(ch) + continue + if ch in "([{": + depth += 1 + current.append(ch) + continue + if ch in ")]}": + depth = max(depth - 1, 0) + current.append(ch) + continue + if ch == ',' and depth == 0: + args.append("".join(current).strip()) + current = [] + continue + current.append(ch) + if current: + args.append("".join(current).strip()) + return args + +def _find_lua_calls(text, function_names): + calls = [] + pattern = re.compile(r'\b(' + "|".join(re.escape(name) for name in function_names) + r')\s*\(') + pos = 0 + while True: + match = pattern.search(text, pos) + if not match: + break + fn = match.group(1) + start = match.end() + idx = start + depth = 1 + in_string = None + escape = False + while idx < len(text): + ch = text[idx] + if in_string: + if escape: + escape = False + elif ch == '\\': + escape = True + elif ch == in_string: + in_string = None + else: + if ch in ("'", '"'): + in_string = ch + elif ch == '(': + depth += 1 + elif ch == ')': + depth -= 1 + if depth == 0: + calls.append((fn, text[start:idx])) + pos = idx + 1 + break + idx += 1 + else: + break + return calls + +def _find_lua_block(text, start_idx, open_char="{", close_char="}"): + depth = 0 + in_string = None + escape = False + for idx in range(start_idx, len(text)): + ch = text[idx] + if in_string: + if escape: + escape = False + elif ch == "\\": + escape = True + elif ch == in_string: + in_string = None + continue + if ch in ("'", '"'): + in_string = ch + continue + if ch == open_char: + depth += 1 + elif ch == close_char: + depth -= 1 + if depth == 0: + return text[start_idx + 1:idx], idx + 1 + return None, None + +def _extract_lua_bind_calls(text): + binds = [] + calls = _find_lua_calls(text, ["bind", "bindm", "hl.bind"]) + for fn, args_text in calls: + args = _split_lua_args(args_text) + if len(args) < 2: + continue + mods = _parse_lua_string(args[0]) + key = _parse_lua_string(args[1]) + if mods is None or key is None: + continue + description = None + desc_match = re.search(r'description\s*=\s*(\"(?:\\.|[^\"])*\"|\'(?:\\.|[^\'])*\')', args_text, re.DOTALL) + if desc_match: + description = _parse_lua_string(desc_match.group(1)) + elif fn == "bindm" and len(args) >= 4: + description = _parse_lua_string(args[3]) + binds.append({ + "mods": mods, + "key": key, + "description": description or "", + }) + return binds + +def _extract_lua_bind_tables(text): + binds = [] + pattern = re.compile(r'\bapp_binds\s*=\s*\{', re.MULTILINE) + for match in pattern.finditer(text): + block, end_idx = _find_lua_block(text, match.end() - 1) + if block is None: + continue + idx = 0 + depth = 0 + in_string = None + escape = False + entry_start = None + while idx < len(block): + ch = block[idx] + if in_string: + if escape: + escape = False + elif ch == "\\": + escape = True + elif ch == in_string: + in_string = None + idx += 1 + continue + if ch in ("'", '"'): + in_string = ch + idx += 1 + continue + if ch == "{": + depth += 1 + if depth == 1: + entry_start = idx + 1 + elif ch == "}": + if depth == 1 and entry_start is not None: + entry_text = block[entry_start:idx] + args = _split_lua_args(entry_text) + if len(args) >= 4: + mods = _parse_lua_string(args[0]) + key = _parse_lua_string(args[1]) + description = _parse_lua_string(args[3]) + if mods is not None and key is not None: + binds.append({ + "mods": mods, + "key": key, + "description": description or "", + }) + entry_start = None + depth = max(depth - 1, 0) + idx += 1 + return binds + +def _extract_lua_binds(text): + binds = [] + binds.extend(_extract_lua_bind_calls(text)) + binds.extend(_extract_lua_bind_tables(text)) + return binds + +def _format_lua_binds(binds): + formatted_lines = [] + for bind in binds: + mods = bind["mods"].replace("$mainMod", "SUPER") + mods = re.sub(r'[ \t]+', '+', mods.strip()) + key = humanize_key_token(mods, bind["key"]) + if mods and key: + combo_str = f"{mods}+{key}" + elif key: + combo_str = key + else: + combo_str = mods + desc = (bind.get("description") or "").strip() + if desc: + formatted_lines.append(f"{combo_str} — {desc}") + else: + formatted_lines.append(combo_str) + return formatted_lines + +def parse_lua_files(files): + order = [] + bind_map = {} + 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: + binds = _extract_lua_binds(f.read()) + except Exception as e: + sys.stderr.write(f"Error reading {file_path}: {e}\n") + continue + for bind in binds: + combo_key = normalize_combo(f"{bind['mods']},{bind['key']}") + if combo_key in bind_map: + try: + order.remove(combo_key) + except ValueError: + pass + bind_map[combo_key] = bind + order.append(combo_key) + effective_binds = [bind_map[key] for key in order if key in bind_map] + return _format_lua_binds(effective_binds) def format_for_rofi(raw_binds): formatted_lines = [] @@ -186,6 +444,7 @@ def format_for_rofi(raw_binds): # Formatting mods mods = mods.replace("$mainMod", "SUPER") mods = re.sub(r'[ \t]+', '+', mods) + key = humanize_key_token(mods, key) # Build combo string if mods and key: @@ -214,7 +473,16 @@ def main(): sys.exit(0) config_files = sys.argv[1:] - + has_lua = any(path.endswith(".lua") for path in config_files) + if has_lua: + formatted = parse_lua_files(config_files) + if not formatted: + print("no keybinds found.") + sys.exit(1) + for line in formatted: + print(line) + return + binds, suggestions = parse_files(config_files) if not binds: |
