diff --git a/src/ai_typewriter/app.py b/src/ai_typewriter/app.py new file mode 100644 index 0000000..d40c741 --- /dev/null +++ b/src/ai_typewriter/app.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +import argparse +import logging +import sys +from threading import Lock + +import keyboard + +from .ai_client import AIClientError, ask_ai +from .clipboard_capture import capture_selection +from .config import AppConfig, load_config +from .key_stepper import KeyStepper + +LOG = logging.getLogger("ai_typewriter") + + +class AITypewriterApp: + def __init__(self, config: AppConfig) -> None: + self.config = config + self._busy = Lock() + self._stepper: KeyStepper | None = None + + def run(self) -> None: + keyboard.add_hotkey(self.config.hotkey, self._handle_hotkey, suppress=False) + LOG.info("Prêt. Raccourci: %s. Quitter: Ctrl+C dans ce terminal.", self.config.hotkey) + keyboard.wait() + + def _handle_hotkey(self) -> None: + if not self._busy.acquire(blocking=False): + LOG.warning("Requête déjà en cours, raccourci ignoré.") + return + try: + self._capture_ask_and_step() + finally: + self._busy.release() + + def _capture_ask_and_step(self) -> None: + try: + selected = capture_selection(self.config.copy_wait_seconds, self.config.restore_clipboard) + LOG.info("Texte capturé: %d caractères.", len(selected)) + answer = ask_ai(selected, self.config) + LOG.info("Réponse reçue: %d caractères. Mode dactylographie actif.", len(answer)) + if self._stepper is not None: + self._stepper.stop() + self._stepper = KeyStepper(answer, delay=self.config.type_delay_seconds) + self._stepper.start() + except (AIClientError, FileNotFoundError, ValueError) as exc: + LOG.error("%s", exc) + except Exception: + LOG.exception("Erreur inattendue") + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="AI Typewriter") + parser.add_argument("--config", default="config.json", help="Chemin du fichier config.json") + parser.add_argument("--debug", action="store_true", help="Logs détaillés") + parser.add_argument("--ask", help="Mode test: envoie ce texte à l'IA et imprime la réponse, sans hook clavier") + return parser + + +def main(argv: list[str] | None = None) -> int: + args = build_parser().parse_args(argv) + logging.basicConfig( + level=logging.DEBUG if args.debug else logging.INFO, + format="%(asctime)s %(levelname)s %(message)s", + ) + try: + config = load_config(args.config) + except Exception as exc: + LOG.error("%s", exc) + return 2 + + if args.ask is not None: + try: + print(ask_ai(args.ask, config)) + return 0 + except AIClientError as exc: + LOG.error("%s", exc) + return 1 + + AITypewriterApp(config).run() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:]))