""" MerlinConfig — load and save user gesture-to-action mappings. Config file: ~/.merlin/config.yaml If the file does not exist, defaults are used and written on first save. """ from __future__ import annotations from dataclasses import dataclass, field, asdict from pathlib import Path from typing import Optional import yaml CONFIG_PATH = Path.home() / ".merlin" / "config.yaml" DEFAULT_MAPPINGS: dict[str, str] = { "left_blink": "left_click", "right_blink": "right_click", "both_blink": "double_click", "head_nod": "key_enter", "head_shake": "key_escape", "head_tilt_left": "scroll_up", "head_tilt_right": "scroll_down", "open_palm": "key_space", "pinch": "drag_start", "fist": "key_ctrl_z", "gaze_left": "scroll_left", "gaze_right": "scroll_right", "gaze_up": "scroll_up", "gaze_down": "scroll_down", } @dataclass class MerlinConfig: mappings: dict[str, str] = field(default_factory=lambda: dict(DEFAULT_MAPPINGS)) confidence_threshold: float = 0.80 camera_device: int = 0 dwell_ms: int = 800 blink_threshold: float = 0.20 @classmethod def load(cls, path: Path = CONFIG_PATH) -> MerlinConfig: if not path.exists(): return cls() with path.open() as f: data = yaml.safe_load(f) or {} return cls( mappings=data.get("mappings", DEFAULT_MAPPINGS), confidence_threshold=float(data.get("confidence_threshold", 0.80)), camera_device=int(data.get("camera_device", 0)), dwell_ms=int(data.get("dwell_ms", 800)), blink_threshold=float(data.get("blink_threshold", 0.20)), ) def save(self, path: Path = CONFIG_PATH) -> None: path.parent.mkdir(parents=True, exist_ok=True) with path.open("w") as f: yaml.safe_dump(asdict(self), f, default_flow_style=False)