Files
agent_v0/key_captor.py
2026-03-05 00:20:23 +01:00

124 lines
3.5 KiB
Python

# 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