#!/usr/bin/env python3 # This script is used to display help information for Hyprland keybinds. import os import sys import re import subprocess HARDCODED_VARIABLES = { "exec, ": "", "VoidSymbol": "CAPS", "brave --proxy-pac-url=\"https://pac.cn01.woodburn.au/proxy.pac\" --enable-features=UseOzonePlatform --ozone-platform=wayland --use-gl=angle --ignore-gpu-blocklist --enable-features=VaapiVideoEncoder,VaapiVideoDecoder,CanvasOopRasterization,VaapiIgnoreDriverChecks,VaapiVideoDecodeLinuxGL,AcceleratedVideoEncoder,Vulkan,DefaultANGLEVulkan,VulkanFromANGLE --disable-gpu-memory-buffer-video-frames": "brave", "~/.config/hypr/scripts/":"󰯂 ", ".sh": "", ".py": "", "alacritty --config-file ~/dotfiles/.alacritty-nozellij.toml": "Terminal (No Zellij)", "ydotool key 56:1 105:1 105:0 56:0":"Back", "ydotool key 56:1 106:1 106:0 56:0":"Forward", "mouse_left":"Left Tilt", "mouse_right":"Right Tilt", "[float] kitty --class float-80 -e":"", "alacritty --class float-80 -e":"", "alacritty --class float -e":"", "$":" ", "mouse:272": "Left Drag", "mouse:273": "Right Drag", "hyprctl": "", "dispatch ": "", "vdesk,": "󰧨", "vdeskreset": "󰧨 Reset", "movetodesk": "󰶭 󰧨", "movetoworkspace": "󰶭 󰧨", "togglespecialworkspace": "󰔡 󰧨", "special:":"", "lastdesk": "󰧨 Last Active", } IGNORED_ACTIONS = [ "hyprctl notify", "submap, reset", "hyprpanel -q" ] TYPE_MAP = { 'bind': ' ', 'bindl': '', 'bindle': 'Lock', 'bindm': '󰍽', } SUBMAP_MAP ={ "main": "", "capsmode": "󰚟󰌎", "opacity": "󰚟󱡔", } KEYCODES = { 20: "-" } # Read the keybinds.conf file def read_keybinds(file_path = os.path.expanduser('~/.config/hypr/keybinds.conf')): if not os.path.exists(file_path): print(f"Error: The file {file_path} does not exist.") sys.exit(1) with open(file_path, 'r') as file: lines = file.readlines() return lines def parse_keybinds(lines): """Parse keybinds from the configuration file and return a list of structured keybind information.""" keybinds = [] variables = {} current_submap = "main" for line_num, line in enumerate(lines, 1): line = line.strip() # Skip empty lines and comments if not line or line.startswith('#'): continue # Handle variable definitions if line.startswith('$'): var_match = re.match(r'\$(\w+)\s*=\s*(.+)', line) if var_match: variables[var_match.group(1)] = var_match.group(2) continue # Handle submap declarations if line.startswith('submap ='): submap_match = re.match(r'submap\s*=\s*(\w+)', line) if submap_match: current_submap = submap_match.group(1) continue # Handle bind declarations if line.startswith('bind') or line.startswith('bindle') or line.startswith('bindl') or line.startswith('bindm'): bind_match = re.match(r'(bind[lme]?)\s*=\s*([^,]+),\s*([^,]+),\s*(.+)', line) if bind_match: bind_type = bind_match.group(1) modifiers = bind_match.group(2).strip() key = bind_match.group(3).strip() action = bind_match.group(4).strip() # Ignore certain actions if any(ignored in action for ignored in IGNORED_ACTIONS): continue # Replace variables in the key, modifiers, and action for var_name, var_value in variables.items(): modifiers = modifiers.replace(f'${var_name}', var_value) key = key.replace(f'${var_name}', var_value) # action = action.replace(f'${var_name}', var_value) # Update for HARDCODED for var_name, var_value in HARDCODED_VARIABLES.items(): modifiers = modifiers.replace(f'{var_name}', var_value) key = key.replace(f'{var_name}', var_value) action = action.replace(f'{var_name}', var_value) if action.startswith('submap,'): smap = action.split(',')[1].strip() action = SUBMAP_MAP.get(smap, smap) # Convert any 'code:' in key to the corresponding Unicode character code_match = re.match(r'code:(\d+)', key) if code_match: code_num = int(code_match.group(1)) key = KEYCODES[code_num] if code_num in KEYCODES else f"code:{code_num}" # Add icon for logging if ">>" in action: log_action = action.split('>>')[0].strip() action = f"{log_action} " # Cleanup action action = action.strip(',') action = action.replace(',', '') # Format the keybind description key_combo = f"{modifiers} + {key}" if modifiers else key keybind_info = { 'type': bind_type, 'submap': current_submap, 'key_combo': key_combo, 'action': action, 'line_num': line_num, 'raw_line': line } keybinds.append(keybind_info) return keybinds def format_keybind_for_display(keybind): """Format a keybind for display in fzf or terminal.""" submap_prefix = f"{SUBMAP_MAP.get(keybind['submap'], keybind['submap'].upper())} " if keybind['submap'] in SUBMAP_MAP else f"{keybind['submap'].upper()} " type_prefix = f"{TYPE_MAP.get(keybind['type'], 'Unknown')} " if keybind['type'] in TYPE_MAP else f"{keybind['type']} " # Clean up the action for display action = keybind['action'] return f"{submap_prefix:3}{type_prefix}{keybind['key_combo']:30} → {action}" def run_fzf_search(keybinds): """Run fzf with the keybinds for interactive searching.""" try: # Prepare the input for fzf fzf_input = [] for keybind in keybinds: formatted = format_keybind_for_display(keybind) fzf_input.append(formatted) # Run fzf process = subprocess.Popen( ['fzf', '--reverse', '--border', '--preview-window=up:3', '--header=Hyprland Keybinds - Press Enter or Esc to exit', ], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) stdout, stderr = process.communicate(input='\n'.join(fzf_input)) if process.returncode == 0 and stdout.strip(): # Find the selected keybind selected_line = stdout.strip() for keybind in keybinds: if format_keybind_for_display(keybind) == selected_line: print(f"\nSelected Keybind Details:") print(f"Key Combination: {keybind['key_combo']}") print(f"Action: {keybind['action']}") print(f"Type: {keybind['type']}") print(f"Submap: {keybind['submap']}") print(f"Line: {keybind['line_num']}") print(f"Raw: {keybind['raw_line']}") break except FileNotFoundError: print("Error: fzf is not installed. Please install fzf to use interactive search.") print("On most distributions: sudo pacman -S fzf # or apt install fzf") return False except Exception as e: print(f"Error running fzf: {e}") return False return True if __name__ == "__main__": # Parse command line arguments - default to fzf unless --no-fzf is specified use_fzf = not (len(sys.argv) > 1 and sys.argv[1] in ['--no-fzf', '-n', 'no-fzf']) # Read and parse keybinds raw_lines = read_keybinds() parsed_keybinds = parse_keybinds(raw_lines) if use_fzf: # Use fzf for interactive searching if not run_fzf_search(parsed_keybinds): # Fallback to regular display if fzf fails use_fzf = False if not use_fzf: print("Hyprland Keybinds Help:") print("=" * 50) print(f"Found {len(parsed_keybinds)} keybinds\n") # Group by submap for better organization submaps = {} for keybind in parsed_keybinds: submap = keybind['submap'] if submap not in submaps: submaps[submap] = [] submaps[submap].append(keybind) # Display keybinds grouped by submap for submap_name, submap_keybinds in submaps.items(): if submap_name != 'main': print(f"\n[{submap_name.upper()} SUBMAP]") print("-" * 30) else: print("[MAIN KEYBINDS]") print("-" * 30) for keybind in submap_keybinds: print(format_keybind_for_display(keybind)) print(f"\n{'-' * 50}") print("Usage: python help.py [--no-fzf] to disable interactive search") print("For more information, refer to the Hyprland documentation.") print("You can also customize your keybinds in the keybinds.conf file.") # Wait for user input before exiting input("\nPress Enter to exit...")