diff --git a/shellgenius/config.py b/shellgenius/config.py index 6394af2..55a48b6 100644 --- a/shellgenius/config.py +++ b/shellgenius/config.py @@ -1,133 +1,142 @@ -"""Configuration management for ShellGenius.""" +"""Configuration loader for ShellGenius.""" import os from pathlib import Path -from typing import Any +from typing import Any, Dict, Optional import yaml -CONFIG_DIR = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "shellgenius" -CONFIG_FILE = CONFIG_DIR / "config.yaml" - class Config: - """Configuration class for ShellGenius.""" + """Configuration management for ShellGenius.""" - def __init__(self, config_path: str | None = None): + def __init__(self, config_path: Optional[str] = None): """Initialize configuration. Args: - config_path: Path to config file + config_path: Path to config.yaml file """ - if config_path is None: - config_path = str(CONFIG_FILE) + self.config_path = config_path or os.environ.get( + "SHELLGENIUS_CONFIG", "config.yaml" + ) + self.config: Dict[str, Any] = self._load_config() - self._config_path = config_path - self._config = self._load_config() - - def _load_config(self) -> dict[str, Any]: - """Load configuration from file. + def _load_config(self) -> Dict[str, Any]: + """Load configuration from YAML file. Returns: - Configuration dictionary + Dictionary containing configuration """ - defaults = { + default_config = { "ollama": { "host": "localhost:11434", - "model": "llama3", + "model": "codellama", + "timeout": 120, }, "safety": { "level": "moderate", + "warn_on_destructive": True, + "allow_dangerous": False, + "blocked_patterns": [ + "rm -rf /", + ":(){:|:&};:", + "chmod 777", + "sudo su", + "dd if=/dev/zero", + ], + }, + "ui": { + "theme": "default", + "show_syntax_highlighting": True, + "auto_suggest": True, + "max_history": 100, + }, + "shell": { + "default": "bash", + "prefer_shebang": True, + }, + "history": { + "enabled": True, + "storage_path": "~/.config/shellgenius/history.yaml", + "similarity_threshold": 0.7, }, - "default_shell": "bash", } - try: - with open(self._config_path) as f: - config = yaml.safe_load(f) or {} - except FileNotFoundError: - config = {} + if self.config_path and Path(self.config_path).exists(): + try: + with open(self.config_path, "r") as f: + user_config = yaml.safe_load(f) or {} + return self._merge_configs(default_config, user_config) + except Exception: + return default_config + return default_config - if isinstance(config, dict): - defaults.update(config) + def _merge_configs( + self, default: Dict[str, Any], user: Dict[str, Any] + ) -> Dict[str, Any]: + """Merge user config with defaults. - return defaults + Args: + default: Default configuration + user: User-provided configuration + + Returns: + Merged configuration + """ + result = default.copy() + for key, value in user.items(): + if key in result and isinstance(result[key], dict) and isinstance(value, dict): + result[key] = self._merge_configs(result[key], value) + else: + result[key] = value + return result def get(self, key: str, default: Any = None) -> Any: """Get configuration value by key. Args: - key: Dot-separated key (e.g., "ollama.host") + key: Dot-separated key path (e.g., "ollama.host") default: Default value if key not found Returns: Configuration value """ keys = key.split(".") - value: Any = self._config - + value = self.config for k in keys: - if isinstance(value, dict): - value = value.get(k) if k in value else None + if isinstance(value, dict) and k in value: + value = value[k] else: return default - - return value if value is not None else default - - def save(self) -> None: - """Save configuration to file.""" - Path(self._config_path).parent.mkdir(parents=True, exist_ok=True) - with open(self._config_path, "w") as f: - yaml.dump(self._config, f) + return value @property def ollama_host(self) -> str: - """Get Ollama host URL.""" - return self.get("ollama.host", "localhost:11434") + """Get Ollama host.""" + return os.environ.get("OLLAMA_HOST", self.get("ollama.host", "localhost:11434")) @property def ollama_model(self) -> str: - """Get Ollama model name.""" - return self.get("ollama.model", "llama3") + """Get Ollama model.""" + return os.environ.get("OLLAMA_MODEL", self.get("ollama.model", "codellama")) @property def safety_level(self) -> str: """Get safety level.""" - return self.get("safety.level", "moderate") + return os.environ.get("SHELLGENIUS_SAFETY", self.get("safety.level", "moderate")) - @property - def default_shell(self) -> str: - """Get default shell type.""" - return self.get("default_shell", "bash") + def reload(self) -> None: + """Reload configuration from file.""" + self.config = self._load_config() -def get_config(config_path: str | None = None) -> dict[str, Any]: - """Load configuration from file or return defaults.""" - if config_path is None: - config_path = str(CONFIG_FILE) +def get_config(config_path: Optional[str] = None) -> Config: + """Get configuration instance. - defaults = { - "ollama_host": "http://localhost:11434", - "ollama_model": "llama3", - "default_shell": "bash", - } + Args: + config_path: Optional path to config file - try: - with open(config_path) as f: - config = yaml.safe_load(f) or {} - except FileNotFoundError: - config = {} - - merged = defaults.copy() - merged.update(config) - return merged - - -def save_config(config: dict[str, Any], config_path: str | None = None) -> None: - """Save configuration to file.""" - if config_path is None: - config_path = str(CONFIG_FILE) - - Path(config_path).parent.mkdir(parents=True, exist_ok=True) - with open(config_path, "w") as f: - yaml.dump(config, f) + Returns: + Config instance + """ + return Config(config_path)