124 lines
3.5 KiB
Python
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
|