"""Everything the node reads from the environment, parsed once at startup. Parsing happens here rather than where each value is used, so an unusable setting stops the node before it joins the dataflow instead of surfacing the first time somebody picks up the gamepad. """ import os from dataclasses import dataclass def env_int(name: str, default: int) -> int: """Read a whole number, naming the variable if it cannot be read as one.""" value = os.environ.get(name, "").strip() if not value: return default try: return int(value) except ValueError: raise ValueError(f"{name} must be a whole number, not {value!r}") from None @dataclass class Config: """The node's settings. Short on purpose. A deadzone, a response curve and a speed all describe what a reading should *do*, and this node does not know — it reads a pad and says so. Adding them here would mean two places shaping the same number. """ joystick_index: int @classmethod def from_env(cls) -> "Config": """Build the configuration from the environment, refusing anything unusable.""" index = env_int("JOYSTICK_INDEX", 0) if index < 0: raise ValueError(f"JOYSTICK_INDEX must not be negative, not {index}") return cls(joystick_index=index)