from __future__ import annotations import configparser from pathlib import Path DEFAULT_KEYBINDINGS: dict[str, list[str]] = { "move_up": ["w", "ArrowUp"], "move_down": ["s", "ArrowDown"], "move_left": ["a", "ArrowLeft"], "move_right": ["d", "ArrowRight"], "leap": ["space"], "block": ["b"], "activate_right_hand": ["mouse_left"], "activate_left_hand": ["mouse_right"], "activate_right_hand_2": ["shift+mouse_left"], "activate_left_hand_2": ["shift+mouse_right"], "select_ability_1": ["1"], "select_ability_2": ["2"], "select_ability_3": ["3"], "select_ability_4": ["4"], "select_ability_5": ["5"], "select_ability_6": ["6"], "select_ability_7": ["7"], "select_ability_8": ["8"], "select_ability_9": ["9"], "select_ability_0": ["0"], } # Human-friendly config names for keys that don't have a printable representation. _KEY_ALIASES = {"space": " "} def _normalize(key: str) -> str: return _KEY_ALIASES.get(key, key) class KeyBindings: """Action -> list of keys, loaded from an editable .conf file (arrays, so several keys can share one action). Falls back to DEFAULT_KEYBINDINGS for anything the file omits. """ def __init__(self, bindings: dict[str, list[str]]): self.bindings = bindings self._action_by_key: dict[str, str] = {} for action, keys in bindings.items(): for key in keys: self._action_by_key[_normalize(key)] = action @classmethod def load(cls, path: Path) -> "KeyBindings": bindings = {action: list(keys) for action, keys in DEFAULT_KEYBINDINGS.items()} if path.exists(): parser = configparser.ConfigParser() parser.read(path) for section in parser.sections(): for action, raw in parser.items(section): keys = [k.strip() for k in raw.split(",") if k.strip()] if keys: bindings[action] = keys return cls(bindings) def action_for_key(self, key: str) -> str | None: return self._action_by_key.get(key) def keys_for_action(self, action: str) -> list[str]: return self.bindings.get(action, [])