Improve clipboard workflow and logging

This commit is contained in:
Hermes Agent
2026-09-11 13:24:39 +02:00
parent e779bd3423
commit 9320217e0f
7 changed files with 117 additions and 45 deletions
+18 -5
View File
@@ -3,12 +3,13 @@ from __future__ import annotations
import argparse
import logging
import sys
import time
from threading import Lock
import keyboard
from .ai_client import AIClientError, ask_ai
from .clipboard_capture import capture_selection
from .clipboard_capture import capture_clipboard
from .config import AppConfig, load_config
from .key_stepper import KeyStepper
@@ -37,10 +38,18 @@ class AITypewriterApp:
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))
selected = capture_clipboard()
LOG.info("Texte lu depuis le presse-papier: %d caractères.", len(selected))
LOG.info("Texte capturé: %r", selected)
started_at = time.perf_counter()
answer = ask_ai(selected, self.config)
LOG.info("Réponse reçue: %d caractères. Mode dactylographie actif.", len(answer))
elapsed = time.perf_counter() - started_at
LOG.info(
"Réponse reçue en %.2f s: %d caractères. Premier caractère écrit automatiquement, %d restants au clavier.",
elapsed,
len(answer),
max(len(answer) - 1, 0),
)
if self._stepper is not None:
self._stepper.stop()
self._stepper = KeyStepper(answer, delay=self.config.type_delay_seconds)
@@ -73,7 +82,11 @@ def main(argv: list[str] | None = None) -> int:
if args.ask is not None:
try:
print(ask_ai(args.ask, config))
started_at = time.perf_counter()
answer = ask_ai(args.ask, config)
elapsed = time.perf_counter() - started_at
LOG.info("Réponse générée en %.2f s: %d caractères.", elapsed, len(answer))
print(answer)
return 0
except AIClientError as exc:
LOG.error("%s", exc)
+7 -23
View File
@@ -1,29 +1,13 @@
from __future__ import annotations
import time
import keyboard
import pyperclip
def capture_clipboard() -> str:
"""Return the current clipboard text without modifying it."""
return pyperclip.paste() or ""
# Backward-compatible alias for older imports/tests.
def capture_selection(copy_wait_seconds: float = 1.0, restore_clipboard: bool = True) -> str:
previous = pyperclip.paste()
keyboard.send("ctrl+c")
deadline = time.monotonic() + max(copy_wait_seconds, 0.05)
captured = previous
while time.monotonic() < deadline:
current = pyperclip.paste()
if current != previous:
captured = current
break
time.sleep(0.03)
else:
captured = pyperclip.paste()
if restore_clipboard:
try:
pyperclip.copy(previous)
except pyperclip.PyperclipException:
pass
return captured or ""
return capture_clipboard()
+18 -9
View File
@@ -21,19 +21,17 @@ class KeyStepper:
return
if self._hook is not None:
return
self.type_next_character()
if self._index >= len(self.text):
return
# suppress=True blocks the physical key while the callback types the next AI character.
self._hook = keyboard.hook(self._on_event, suppress=True)
def stop(self) -> None:
hook = self._hook
self._hook = None
if hook is not None:
keyboard.unhook(hook)
def _on_event(self, event: keyboard.KeyboardEvent) -> None:
if self._injecting or event.event_type != keyboard.KEY_DOWN:
return
@property
def remaining_characters(self) -> int:
return max(len(self.text) - self._index, 0)
def type_next_character(self) -> None:
with self._lock:
if self._index >= len(self.text):
self.stop()
@@ -50,6 +48,17 @@ class KeyStepper:
if self._index >= len(self.text):
self.stop()
def stop(self) -> None:
hook = self._hook
self._hook = None
if hook is not None:
keyboard.unhook(hook)
def _on_event(self, event: keyboard.KeyboardEvent) -> None:
if self._injecting or event.event_type != keyboard.KEY_DOWN:
return
self.type_next_character()
def _type_char(self, char: str) -> None:
if char == "\n":
keyboard.send("enter")