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
This commit is contained in:
+209
@@ -0,0 +1,209 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user