import re import secrets import sys from dataclasses import dataclass from pathlib import Path try: import tomllib except ImportError: try: import tomli as tomllib # type: ignore[no-redef] except ImportError: print("Error: Python 3.11+ is required (for tomllib), or install tomli: pip install tomli") sys.exit(1) CONFIG_PATH = Path("config.toml") @dataclass class AppConfig: width: int | None height: int | None fps: int clock_font_path: str message_font_path: str port: int default_duration: float api_token: str rate_limit: int password_hash: str session_timeout_hours: int config_path: Path @classmethod def load(cls, path: str | Path = CONFIG_PATH) -> "AppConfig": p = Path(path) if not p.exists(): print(f"Error: Config file not found: {p}") print("Copy config.toml to your working directory and edit it.") sys.exit(1) with open(p, "rb") as f: raw = tomllib.load(f) display = raw.get("display", {}) server = raw.get("server", {}) api = raw.get("api", {}) rate = raw.get("rate_limit", {}) dashboard = raw.get("dashboard", {}) token = api.get("token", "").strip() if not token: token = secrets.token_hex(32) _update_config_field(p, "token", token) print(f"\n[KH-clock] Generated API bearer token (saved to config.toml):") print(f" {token}\n") return cls( width=display.get("width"), height=display.get("height"), fps=display.get("fps", 10), clock_font_path=display.get("clock_font_path", "").strip(), message_font_path=display.get("message_font_path", "").strip(), port=server.get("port", 8080), default_duration=float(server.get("default_duration_seconds", 20)), api_token=token, rate_limit=rate.get("requests_per_minute", 20), password_hash=dashboard.get("password_hash", "").strip(), session_timeout_hours=dashboard.get("session_timeout_hours", 8), config_path=p, ) def save_password_hash(self, hashed: str) -> None: _update_config_field(self.config_path, "password_hash", hashed) self.password_hash = hashed def _update_config_field(config_path: Path, key: str, value: str) -> None: """Update a quoted string field in config.toml using regex replacement.""" content = config_path.read_text() pattern = rf'^({re.escape(key)}\s*=\s*)"[^"]*"' def replacer(m: re.Match) -> str: return f'{m.group(1)}"{value}"' new_content = re.sub(pattern, replacer, content, flags=re.MULTILINE) if new_content == content: print(f"Warning: Could not update '{key}' in {config_path}. Field not found.") return config_path.write_text(new_content)