Initial commit
This commit is contained in:
123
key_captor.py
Normal file
123
key_captor.py
Normal file
@@ -0,0 +1,123 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user