""" Configuration management for GSPro Remote. """ import json import logging from pathlib import Path from typing import Optional from functools import lru_cache from pydantic import BaseModel, Field from pydantic_settings import BaseSettings logger = logging.getLogger(__name__) class ServerConfig(BaseModel): """Server configuration settings.""" host: str = Field("0.0.0.0", description="Server host address") port: int = Field(5005, description="Server port") mdns_enabled: bool = Field(True, description="Enable mDNS service discovery") class CaptureConfig(BaseModel): """Screen capture configuration settings.""" fps: int = Field(30, description="Frames per second for streaming") quality: int = Field(85, description="JPEG quality (0-100)") resolution: str = Field("720p", description="Stream resolution") region_x: int = Field(0, description="Map region X coordinate") region_y: int = Field(0, description="Map region Y coordinate") region_width: int = Field(640, description="Map region width") region_height: int = Field(480, description="Map region height") class GSProConfig(BaseModel): """GSPro application configuration settings.""" window_title: str = Field("GSPro", description="GSPro window title") auto_focus: bool = Field(True, description="Auto-focus GSPro window before sending keys") key_delay: float = Field(0.05, description="Default delay between key presses (seconds)") class VisionConfig(BaseModel): """Computer vision configuration settings (for V2 features).""" enabled: bool = Field(False, description="Enable vision features") ocr_engine: str = Field("easyocr", description="OCR engine to use (easyocr or tesseract)") confidence_threshold: float = Field(0.7, description="Minimum confidence for OCR detection") class AppConfig(BaseSettings): """Main application configuration.""" server: ServerConfig = Field(default_factory=ServerConfig) capture: CaptureConfig = Field(default_factory=CaptureConfig) gspro: GSProConfig = Field(default_factory=GSProConfig) vision: VisionConfig = Field(default_factory=VisionConfig) config_path: Optional[Path] = None debug: bool = Field(False, description="Enable debug mode") class Config: env_prefix = "GSPRO_REMOTE_" env_nested_delimiter = "__" case_sensitive = False def __init__(self, **kwargs): super().__init__(**kwargs) if self.config_path is None: self.config_path = self._get_default_config_path() self.load() @staticmethod def _get_default_config_path() -> Path: """Get the default configuration file path.""" import os if os.name == "nt": # Windows base_path = Path(os.environ.get("LOCALAPPDATA", "")) if not base_path: base_path = Path.home() / "AppData" / "Local" else: # Unix-like base_path = Path.home() / ".config" config_dir = base_path / "GSPro Remote" config_dir.mkdir(parents=True, exist_ok=True) return config_dir / "config.json" def load(self) -> None: """Load configuration from file.""" if self.config_path and self.config_path.exists(): try: with open(self.config_path, "r") as f: data = json.load(f) # Update configuration with loaded data if "server" in data: self.server = ServerConfig(**data["server"]) if "capture" in data: self.capture = CaptureConfig(**data["capture"]) if "gspro" in data: self.gspro = GSProConfig(**data["gspro"]) if "vision" in data: self.vision = VisionConfig(**data["vision"]) if "debug" in data: self.debug = data["debug"] logger.info(f"Configuration loaded from {self.config_path}") except Exception as e: logger.warning(f"Failed to load configuration: {e}") self.save() # Save default configuration else: # Create default configuration file self.save() logger.info(f"Created default configuration at {self.config_path}") def save(self) -> None: """Save configuration to file.""" if self.config_path: try: self.config_path.parent.mkdir(parents=True, exist_ok=True) data = { "server": self.server.model_dump(), "capture": self.capture.model_dump(), "gspro": self.gspro.model_dump(), "vision": self.vision.model_dump(), "debug": self.debug, } with open(self.config_path, "w") as f: json.dump(data, f, indent=2) logger.info(f"Configuration saved to {self.config_path}") except Exception as e: logger.error(f"Failed to save configuration: {e}") def update(self, **kwargs) -> None: """Update configuration with new values.""" for key, value in kwargs.items(): if hasattr(self, key): if isinstance(value, dict): # Update nested configuration current = getattr(self, key) if isinstance(current, BaseModel): for sub_key, sub_value in value.items(): if hasattr(current, sub_key): setattr(current, sub_key, sub_value) else: setattr(self, key, value) self.save() def reset(self) -> None: """Reset configuration to defaults.""" self.server = ServerConfig() self.capture = CaptureConfig() self.gspro = GSProConfig() self.vision = VisionConfig() self.debug = False self.save() def to_dict(self) -> dict: """Convert configuration to dictionary.""" return { "server": self.server.model_dump(), "capture": self.capture.model_dump(), "gspro": self.gspro.model_dump(), "vision": self.vision.model_dump(), "debug": self.debug, "config_path": str(self.config_path) if self.config_path else None, } # Global configuration instance _config: Optional[AppConfig] = None @lru_cache(maxsize=1) def get_config() -> AppConfig: """Get the global configuration instance.""" global _config if _config is None: _config = AppConfig() return _config def reset_config() -> None: """Reset the global configuration instance.""" global _config _config = None get_config.cache_clear()