# key_captor.py """ Capture des combos clavier pour agent_v0. v0 : - écoute globale clavier via pynput - maintient un set de touches "modificateurs" enfoncées (CTRL, ALT, SHIFT, CMD) - quand une touche non-modif est pressée avec au moins un modif, appelle un callback (keys: List[str]) du type ["CTRL", "F"] """ from __future__ import annotations from typing import Callable, List, Set from pynput import keyboard KeyComboCallback = Callable[[List[str]], None] MODIFIER_MAP = { keyboard.Key.ctrl_l: "CTRL", keyboard.Key.ctrl_r: "CTRL", keyboard.Key.alt_l: "ALT", keyboard.Key.alt_r: "ALT", keyboard.Key.shift: "SHIFT", keyboard.Key.shift_l: "SHIFT", keyboard.Key.shift_r: "SHIFT", keyboard.Key.cmd: "CMD", keyboard.Key.cmd_l: "CMD", keyboard.Key.cmd_r: "CMD", } class KeyCaptor: """ Capture les combos clavier globaux et les relaie à un callback. """ def __init__(self, on_key_combo: KeyComboCallback) -> None: """ :param on_key_combo: fonction appelée sur combo clavier, signature (keys: List[str]) """ self._on_key_combo = on_key_combo self._listener: keyboard.Listener | None = None self._mods_pressed: Set[str] = set() def start(self) -> None: """Démarre l'écoute globale clavier.""" if self._listener is not None: return self._listener = keyboard.Listener( on_press=self._handle_press, on_release=self._handle_release, ) self._listener.start() def stop(self) -> None: """Arrête l'écoute globale clavier.""" if self._listener is not None: self._listener.stop() self._listener = None self._mods_pressed.clear() # --- callbacks internes --- def _handle_press(self, key: keyboard.Key | keyboard.KeyCode) -> None: # Gestion des mods mod_name = MODIFIER_MAP.get(key) if mod_name: self._mods_pressed.add(mod_name) return # Ici : touche non-modif if not self._mods_pressed: # Pas de modif → on ignore (on ne log pas la frappe "normale") return # On construit la liste des touches keys = sorted(self._mods_pressed) # Nom de la touche principale main_key = self._key_to_str(key) if main_key is None: return keys.append(main_key) # Appel du callback try: self._on_key_combo(keys) except Exception: # On évite de casser le listener en cas d'erreur dans le callback pass def _handle_release(self, key: keyboard.Key | keyboard.KeyCode) -> None: # Retirer les modifs qui ne sont plus pressées mod_name = MODIFIER_MAP.get(key) if mod_name and mod_name in self._mods_pressed: self._mods_pressed.discard(mod_name) @staticmethod def _key_to_str(key: keyboard.Key | keyboard.KeyCode) -> str | None: # Touche "normale" (KeyCode avec .char) if isinstance(key, keyboard.KeyCode): if key.char: return key.char.upper() return None # Quelques touches "spéciales" qu'on peut vouloir logguer special_map = { keyboard.Key.enter: "ENTER", keyboard.Key.tab: "TAB", keyboard.Key.esc: "ESC", keyboard.Key.space: "SPACE", } if key in special_map: return special_map[key] # Sinon, on ignore return None