from __future__ import annotations from dataclasses import dataclass import json import os from pathlib import Path from threading import Lock PROJECT_ROOT = Path(__file__).resolve().parent.parent CONFIG_PATH = PROJECT_ROOT / "config.json" DEVICE_ROLE_ENV_VAR = "KVM_DEVICE_ROLE" TOWER_TRIGGER_CODE = 15 LAPTOP_TRIGGER_CODE = 19 SUPPORTED_DEVICE_ROLES = {"tower", "laptop"} SUPPORTED_TARGET_PORTS = { "DP1": {"ddm_input": "DP1", "input_codes": {15}}, "DP2": {"ddm_input": "DP2", "input_codes": {19}}, "HDMI": {"ddm_input": "HDMI", "input_codes": {17}}, } @dataclass(slots=True) class AppConfig: device_role: str | None = None device_port: str = "DP1" @classmethod def from_dict(cls, data: dict[str, object], default_role: str | None = None) -> "AppConfig": return cls( device_role=_coerce_role_name(data.get("device_role"), default_role), device_port=_coerce_port_name(data.get("device_port"), "DP1"), ) def to_dict(self) -> dict[str, str | None]: return { "device_role": self.device_role, "device_port": self.device_port, } def validate(self) -> list[str]: errors: list[str] = [] if self.device_role not in SUPPORTED_DEVICE_ROLES: errors.append( f"Device role is not configured. Set it in the UI, set {DEVICE_ROLE_ENV_VAR} to TOWER or LAPTOP, or add device_role to config.json." ) supported = ", ".join(SUPPORTED_TARGET_PORTS) if self.device_port not in SUPPORTED_TARGET_PORTS: errors.append(f"Device Port must be one of: {supported}.") return errors class ConfigStore: def __init__(self, path: Path = CONFIG_PATH): self.path = path self._lock = Lock() self._default_role = _coerce_role_name(os.environ.get(DEVICE_ROLE_ENV_VAR), None) self._config = self._load_from_disk() def get(self) -> AppConfig: with self._lock: return AppConfig( device_role=self._config.device_role, device_port=self._config.device_port, ) def save(self, config: AppConfig) -> AppConfig: errors = config.validate() if errors: raise ValueError("; ".join(errors)) with self._lock: self.path.write_text( json.dumps(config.to_dict(), indent=2), encoding="utf-8", ) self._config = AppConfig( device_role=config.device_role, device_port=config.device_port, ) return AppConfig( device_role=self._config.device_role, device_port=self._config.device_port, ) def _load_from_disk(self) -> AppConfig: if not self.path.exists(): return AppConfig(device_role=self._default_role) try: data = json.loads(self.path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): return AppConfig(device_role=self._default_role) if not isinstance(data, dict): return AppConfig(device_role=self._default_role) return AppConfig.from_dict(data, default_role=self._default_role) def _coerce_port_name(value: object, default: str) -> str: if isinstance(value, str): normalized = value.strip().upper() if normalized in SUPPORTED_TARGET_PORTS: return normalized return default def _coerce_role_name(value: object, default: str | None) -> str | None: if isinstance(value, str): normalized = value.strip().lower() if normalized in SUPPORTED_DEVICE_ROLES: return normalized return default