qlpycon/lib/settings.py
xbl 7ca9795a39 modified: .gitignore
modified:   README.md
	new file:   install.sh
	renamed:    config.py -> lib/constants.py
	renamed:    cvars.py -> lib/cvars.py
	renamed:    formatter.py -> lib/formatter.py
	renamed:    network.py -> lib/network.py
	renamed:    parser.py -> lib/parser.py
	renamed:    qlpycon_config.py -> lib/settings.py
	renamed:    state.py -> lib/state.py
	renamed:    ui.py -> lib/ui.py
	modified:   main.py
	modified:   qlpycon.bash
	new file:   qlpycon.conf.example
2026-06-13 10:21:29 +02:00

210 lines
6.1 KiB
Python

#!/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()