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
210 lines
6.1 KiB
Python
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()
|