from __future__ import annotations from dataclasses import dataclass, field from engine.config import DEFAULT_KEYBINDINGS, KeyBindings # Keys a rebind capture should never accept - they're the menu's own navigation/escape keys, # so binding a game action to one of them would make the controls menu itself unusable. RESERVED_CAPTURE_KEYS = {"Escape", "ArrowUp", "ArrowDown", "Enter"} @dataclass class ControlsMenu: """Pure interaction state for the "configure controls" screen: list every bindable action, navigate up/down, and rebind the highlighted action's primary key to whatever key is pressed next. Reads/writes through the same KeyBindings the game itself uses (engine/ config.py) - a rebind takes effect immediately (live) and can be written back to disk via save(). No rendering here - same boundary as the rest of engine/ui.py. """ keybindings: KeyBindings actions: list[str] = field(default_factory=lambda: list(DEFAULT_KEYBINDINGS)) selected_index: int = 0 awaiting_key: bool = False def move(self, delta: int) -> None: self.selected_index = (self.selected_index + delta) % len(self.actions) def selected_action(self) -> str: return self.actions[self.selected_index] def begin_rebind(self) -> None: self.awaiting_key = True def cancel_rebind(self) -> None: self.awaiting_key = False def capture_key(self, key: str) -> bool: """Feeds the next raw keypress into the pending rebind, if one is in progress (see begin_rebind). Returns whether it was actually applied - a reserved menu key is ignored (capture mode stays open so the player can try again) rather than rebinding onto it. """ if not self.awaiting_key: return False if key in RESERVED_CAPTURE_KEYS: return False self.keybindings.rebind(self.selected_action(), key) self.awaiting_key = False return True def save(self, path) -> None: self.keybindings.save(path)