""" EU-Utility - Settings Manager ============================= User preferences and configuration management with type safety. Features: - Type-safe setting access - Automatic persistence - Signal-based change notifications - Plugin enablement tracking Quick Start: ------------ from core.settings import get_settings settings = get_settings() # Get a setting theme = settings.get('overlay_theme', 'dark') # Set a setting settings.set('overlay_theme', 'light') # Check if plugin is enabled if settings.is_plugin_enabled('my_plugin'): # Load plugin # Connect to changes settings.setting_changed.connect(on_setting_changed) Configuration File: ------------------- Settings are stored in `data/settings.json` in JSON format. The file is automatically created on first run with defaults. """ import json from pathlib import Path from typing import Any, Dict, List, Optional try: from PyQt6.QtCore import QObject, pyqtSignal HAS_QT = True except ImportError: # Fallback for non-Qt environments class QObject: # type: ignore pass class pyqtSignal: # type: ignore def __init__(self, *args: Any) -> None: pass def connect(self, slot: Any) -> None: pass def emit(self, *args: Any) -> None: pass HAS_QT = False class Settings(QObject): """Application settings manager. Provides type-safe access to user preferences with automatic persistence and change notifications. Attributes: setting_changed: Qt signal emitted when a setting changes """ setting_changed = pyqtSignal(str, object) # key, value # Default settings DEFAULTS: Dict[str, Any] = { # Overlay 'overlay_enabled': True, 'overlay_opacity': 0.9, 'overlay_theme': 'dark', # Hotkeys 'hotkey_toggle': 'ctrl+shift+u', 'hotkey_search': 'ctrl+shift+f', 'hotkey_calculator': 'ctrl+shift+c', 'hotkey_music': 'ctrl+shift+m', 'hotkey_scan': 'ctrl+shift+r', 'hotkey_skills': 'ctrl+shift+s', # Plugins 'enabled_plugins': [ 'universal_search', 'calculator', 'spotify_controller', 'nexus_search', 'game_reader', 'skill_scanner', ], 'disabled_plugins': [], # Dashboard 'dashboard_widgets': [ {'type': 'QuickActions', 'row': 0, 'col': 0}, {'type': 'PEDTracker', 'row': 1, 'col': 0}, {'type': 'SkillProgress', 'row': 1, 'col': 1}, ], # Overlay widgets 'overlay_widgets': { 'spotify': {'enabled': False, 'x': 0, 'y': 100}, 'skillgain': {'enabled': False, 'x': 0, 'y': 200}, }, # Game Reader 'ocr_engine': 'easyocr', 'auto_capture': False, 'capture_region': 'full', # Skill Scanner 'auto_track_gains': True, 'skill_alert_threshold': 100, # Appearance 'icon_size': 24, 'accent_color': '#4a9eff', 'show_tooltips': True, # Updates 'check_updates': True, 'auto_update_plugins': False, # Data 'data_retention_days': 30, 'auto_export': False, } def __init__(self, config_file: str = "data/settings.json") -> None: """Initialize settings manager. Args: config_file: Path to settings JSON file """ super().__init__() self.config_file = Path(config_file) self._settings: Dict[str, Any] = {} self._load() def _load(self) -> None: """Load settings from file.""" self._settings = self.DEFAULTS.copy() if self.config_file.exists(): try: with open(self.config_file, 'r', encoding='utf-8') as f: saved = json.load(f) self._settings.update(saved) except (json.JSONDecodeError, IOError) as e: print(f"[Settings] Error loading settings: {e}") def save(self) -> None: """Save settings to file.""" try: self.config_file.parent.mkdir(parents=True, exist_ok=True) with open(self.config_file, 'w', encoding='utf-8') as f: json.dump(self._settings, f, indent=2) except IOError as e: print(f"[Settings] Error saving settings: {e}") def get(self, key: str, default: Any = None) -> Any: """Get a setting value. Args: key: Setting key default: Default value if key not found Returns: Setting value or default """ return self._settings.get(key, default) def set(self, key: str, value: Any) -> None: """Set a setting value. Args: key: Setting key value: Value to store (must be JSON serializable) """ old_value = self._settings.get(key) self._settings[key] = value self.save() self.setting_changed.emit(key, value) def reset(self, key: Optional[str] = None) -> None: """Reset setting(s) to default. Args: key: Specific key to reset, or None to reset all """ if key: self._settings[key] = self.DEFAULTS.get(key) self.save() self.setting_changed.emit(key, self._settings[key]) else: self._settings = self.DEFAULTS.copy() self.save() def is_plugin_enabled(self, plugin_id: str) -> bool: """Check if a plugin is enabled. Args: plugin_id: Unique plugin identifier Returns: True if plugin is enabled """ return plugin_id in self._settings.get('enabled_plugins', []) def enable_plugin(self, plugin_id: str) -> None: """Enable a plugin. Args: plugin_id: Unique plugin identifier """ enabled = self._settings.get('enabled_plugins', []) disabled = self._settings.get('disabled_plugins', []) if plugin_id not in enabled: enabled = enabled + [plugin_id] if plugin_id in disabled: disabled.remove(plugin_id) self.set('enabled_plugins', enabled) self.set('disabled_plugins', disabled) def disable_plugin(self, plugin_id: str) -> None: """Disable a plugin. Args: plugin_id: Unique plugin identifier """ enabled = self._settings.get('enabled_plugins', []) disabled = self._settings.get('disabled_plugins', []) if plugin_id in enabled: enabled.remove(plugin_id) if plugin_id not in disabled: disabled = disabled + [plugin_id] self.set('enabled_plugins', enabled) self.set('disabled_plugins', disabled) def all_settings(self) -> Dict[str, Any]: """Get all settings. Returns: Dictionary with all settings """ return self._settings.copy() # Global settings instance _settings_instance: Optional[Settings] = None def get_settings() -> Settings: """Get global settings instance. Returns: The singleton Settings instance """ global _settings_instance if _settings_instance is None: _settings_instance = Settings() return _settings_instance def reset_settings() -> None: """Reset settings to defaults.""" global _settings_instance if _settings_instance is not None: _settings_instance.reset() __all__ = ['Settings', 'get_settings', 'reset_settings']