""" EU-Utility - Configurable Hotkey Manager Manages global and local hotkeys with user customization. """ import json from pathlib import Path from typing import Dict, Callable, Optional, List from dataclasses import dataclass, asdict from enum import Enum class HotkeyScope(Enum): """Scope of hotkey action.""" GLOBAL = "global" # Works even when app not focused LOCAL = "local" # Only when app is focused OVERLAY = "overlay" # Only when overlay is visible @dataclass class HotkeyConfig: """Configuration for a single hotkey.""" action: str keys: str scope: str enabled: bool = True description: str = "" def to_dict(self) -> dict: return asdict(self) @classmethod def from_dict(cls, data: dict) -> 'HotkeyConfig': return cls(**data) class HotkeyManager: """Manages configurable hotkeys for EU-Utility.""" DEFAULT_HOTKEYS = { 'toggle_overlay': HotkeyConfig( action='toggle_overlay', keys='ctrl+shift+u', scope='global', enabled=True, description='Toggle main overlay window' ), 'hide_overlays': HotkeyConfig( action='hide_overlays', keys='ctrl+shift+h', scope='global', enabled=True, description='Hide all overlays' ), 'quick_search': HotkeyConfig( action='quick_search', keys='ctrl+shift+f', scope='global', enabled=True, description='Open universal search' ), 'nexus_search': HotkeyConfig( action='nexus_search', keys='ctrl+shift+n', scope='global', enabled=True, description='Open Nexus search' ), 'calculator': HotkeyConfig( action='calculator', keys='ctrl+shift+c', scope='global', enabled=True, description='Open calculator' ), 'spotify': HotkeyConfig( action='spotify', keys='ctrl+shift+m', scope='global', enabled=True, description='Toggle Spotify controller' ), 'game_reader': HotkeyConfig( action='game_reader', keys='ctrl+shift+r', scope='global', enabled=True, description='Open game reader (OCR)' ), 'skill_scanner': HotkeyConfig( action='skill_scanner', keys='ctrl+shift+s', scope='global', enabled=True, description='Open skill scanner' ), 'loot_tracker': HotkeyConfig( action='loot_tracker', keys='ctrl+shift+l', scope='global', enabled=True, description='Open loot tracker' ), 'screenshot': HotkeyConfig( action='screenshot', keys='ctrl+shift+p', scope='global', enabled=True, description='Take screenshot' ), 'close_overlay': HotkeyConfig( action='close_overlay', keys='esc', scope='overlay', enabled=True, description='Close overlay (when visible)' ), 'toggle_theme': HotkeyConfig( action='toggle_theme', keys='ctrl+t', scope='local', enabled=True, description='Toggle dark/light theme' ), } def __init__(self, config_path: str = "config/hotkeys.json"): self.config_path = Path(config_path) self.hotkeys: Dict[str, HotkeyConfig] = {} self._handlers: Dict[str, Callable] = {} self._registered_globals: List[str] = [] self._load_config() def _load_config(self): """Load hotkey configuration from file.""" if self.config_path.exists(): try: with open(self.config_path, 'r') as f: data = json.load(f) for key, config_dict in data.items(): self.hotkeys[key] = HotkeyConfig.from_dict(config_dict) # Add any missing defaults for key, default in self.DEFAULT_HOTKEYS.items(): if key not in self.hotkeys: self.hotkeys[key] = default except Exception as e: print(f"[HotkeyManager] Failed to load config: {e}") self.hotkeys = self.DEFAULT_HOTKEYS.copy() else: self.hotkeys = self.DEFAULT_HOTKEYS.copy() self._save_config() def _save_config(self): """Save hotkey configuration to file.""" try: self.config_path.parent.mkdir(parents=True, exist_ok=True) data = {k: v.to_dict() for k, v in self.hotkeys.items()} with open(self.config_path, 'w') as f: json.dump(data, f, indent=2) except Exception as e: print(f"[HotkeyManager] Failed to save config: {e}") def get_hotkey(self, action: str) -> Optional[HotkeyConfig]: """Get hotkey configuration for an action.""" return self.hotkeys.get(action) def set_hotkey(self, action: str, keys: str, scope: str = None, enabled: bool = None): """Update hotkey configuration.""" if action in self.hotkeys: config = self.hotkeys[action] config.keys = keys if scope is not None: config.scope = scope if enabled is not None: config.enabled = enabled self._save_config() return True return False def register_handler(self, action: str, handler: Callable): """Register a handler function for an action.""" self._handlers[action] = handler def get_all_hotkeys(self) -> Dict[str, HotkeyConfig]: """Get all hotkey configurations.""" return self.hotkeys.copy() def validate_key_combo(self, keys: str) -> tuple[bool, str]: """Validate a key combination string. Returns: (is_valid, error_message) """ if not keys or not isinstance(keys, str): return False, "Key combination cannot be empty" # Check for valid format parts = keys.lower().split('+') if len(parts) < 1: return False, "Invalid key combination format" # Valid modifiers modifiers = {'ctrl', 'alt', 'shift', 'cmd', 'win'} # Valid keys valid_keys = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12', 'esc', 'escape', 'tab', 'space', 'enter', 'return', 'backspace', 'delete', 'insert', 'home', 'end', 'pageup', 'pagedown', 'up', 'down', 'left', 'right', 'print', 'scrolllock', 'pause', } for part in parts: part = part.strip() if part in modifiers: continue if part in valid_keys: continue return False, f"Invalid key: '{part}'" return True, "" def reset_to_defaults(self): """Reset all hotkeys to defaults.""" self.hotkeys = self.DEFAULT_HOTKEYS.copy() self._save_config() def get_conflicts(self) -> List[tuple]: """Find conflicting hotkey combinations.""" conflicts = [] seen = {} for action, config in self.hotkeys.items(): if not config.enabled: continue key = (config.keys.lower(), config.scope) if key in seen: conflicts.append((seen[key], action, config.keys)) else: seen[key] = action return conflicts