#!/usr/bin/env python3 """ Configuration file handler for QLPyCon Supports loading from ~/.qlpycon.conf or ./qlpycon.conf """ import os import configparser import logging logger = logging.getLogger('config_loader') class ConfigLoader: """Load configuration from INI file""" def __init__(self): self.config = configparser.ConfigParser() self.config_loaded = False def load(self): """ Try to load config from (in order): 1. ./qlpycon.conf (current directory) 2. ~/.qlpycon.conf (home directory) """ config_paths = [ 'qlpycon.conf', os.path.expanduser('~/.qlpycon.conf') ] for path in config_paths: if os.path.exists(path): try: self.config.read(path) self.config_loaded = True logger.info(f'Loaded configuration from: {path}') return True except Exception as e: logger.warning(f'Failed to load config from {path}: {e}') logger.debug('No configuration file found, using defaults') return False def get(self, section, key, fallback=None): """Get a configuration value""" if not self.config_loaded: return fallback try: return self.config.get(section, key, fallback=fallback) except (configparser.NoSectionError, configparser.NoOptionError): return fallback def get_int(self, section, key, fallback=0): """Get an integer configuration value""" value = self.get(section, key) if value is None: return fallback try: return int(value) except ValueError: logger.warning(f'Invalid integer value for [{section}] {key}: {value}') return fallback def get_bool(self, section, key, fallback=False): """Get a boolean configuration value""" value = self.get(section, key) if value is None: return fallback return value.lower() in ('true', 'yes', '1', 'on') def get_host(self): """Get connection host""" return self.get('connection', 'host') def get_password(self): """Get connection password (supports ${ENV_VAR} syntax)""" password = self.get('connection', 'password') if not password: return None return self._resolve_password(password) def get_servers(self): """Return dict of name -> host:port from [servers]""" if not self.config.has_section('servers'): return {} return dict(self.config.items('servers')) def get_server(self, name): """ Resolve a named server to (host, password). host comes from [servers], password from [server:name] or [connection]. Returns (host, password) or (None, None) if name not found. """ servers = self.get_servers() if name not in servers: return None, None host = servers[name] if not host.startswith('tcp://'): host = f'tcp://{host}' # Per-server password override section = f'server:{name}' if self.config.has_section(section): password = self.config.get(section, 'password', fallback=None) if password: password = self._resolve_password(password) else: password = self.get_password() return host, password def _resolve_password(self, password): """Resolve a password string, expanding ${VAR:-default} if needed""" if not password: return None if password.startswith('${') and password.endswith('}'): inner = password[2:-1] if ':-' in inner: env_var, default = inner.split(':-', 1) else: env_var, default = inner, None return os.environ.get(env_var, default) return password def get_log_level(self): """Get logging level""" level_str = self.get('logging', 'level', 'INFO') levels = { 'DEBUG': logging.DEBUG, 'INFO': logging.INFO, 'WARNING': logging.WARNING, 'ERROR': logging.ERROR, 'CRITICAL': logging.CRITICAL } return levels.get(level_str.upper(), logging.INFO) def create_example_config(): """Create an example configuration file""" config_content = """# qlpycon.conf # Edit this file as needed. # # Connect by server name: qlpycon ffa # Connect directly: qlpycon --host tcp://1.2.3.4:28960 --password secret # List servers: qlpycon --list # === Connection defaults ===> # Default host if no server name or --host is given [connection] host = tcp://127.0.0.1:28960 # Password for all servers unless overridden in [server:name] # Use ${ENV_VAR} to read from environment variable (recommended) # Or set directly (less secure): password = ${QLPYCON_PASSWORD:-secret} # === Named servers ===> # Simple entries: name = host:port # These use the password from [connection] above. [servers] # Example: #ffa = 10.13.12.161:28960 # === Per-server overrides ===> # Use [server:name] to override any setting for a specific server. # The name must match an entry in [servers] above. # Example: #[server:ffa] #password = ${FFA_PASSWORD:-secret} # === Logging ===> [logging] # Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL level = WARNING # === UI ===> [ui] # Number of commands to remember in history max_history = 10 # === Behaviour ===> [behavior] # Seconds to confirm quit (press Ctrl-C twice within this window) quit_timeout = 3.0 # Seconds before players respawn after death respawn_delay = 3.0 """ example_path = os.path.expanduser('~/.qlpycon.conf.example') try: with open(example_path, 'w') as f: f.write(config_content) print(f'Created example config: {example_path}') print(f'Copy to ~/.qlpycon.conf and edit as needed') return True except Exception as e: print(f'Failed to create example config: {e}') return False if __name__ == '__main__': # Create example config when run directly create_example_config()