""" Terminal UI con scrollback usando Pyte Versione semplificata e robusta """ import os import sys import signal import shutil from typing import Optional, Callable try: import pyte PYTE_AVAILABLE = True except ImportError: PYTE_AVAILABLE = False class TerminalUI: """ Terminal emulator con scrollback usando pyte. Features: - Scrollback buffer completo (10000 righe) - Output colorato con ANSI - Resize del terminale """ def __init__( self, scrollback: int = 10000, prompt: str = "❯ " ): """ Inizializza il terminale emulator. Args: scrollback: Numero di righe nello scrollback buffer prompt: Prompt da mostrare """ if not PYTE_AVAILABLE: raise ImportError("pyte non è installato. Esegui: pip install pyte") self.scrollback = scrollback self.prompt = prompt # Dimensioni terminale self.term_rows, self.term_cols = shutil.get_terminal_size() self.term_rows = max(24, min(self.term_rows, 50)) self.term_cols = max(80, min(self.term_cols, 120)) # Crea lo screen pyte self.screen = pyte.Screen(self.term_cols, self.term_rows) self.screen.scroll_history = scrollback self.stream = pyte.Stream(self.screen) # Buffer per l'output completo (per scrollback) self.output_buffer: list = [] # Flag per uscire self.running = False # Callback per l'agent self.agent_callback: Optional[Callable[[str], str]] = None def _redraw(self): """Ridisegna lo schermo""" # Clear schermo sys.stdout.write("\033[2J\033[H") # Header header = f"╔{'═' * (self.term_cols - 4)}╗" title = f" LangCode Agent - Terminal con Scrollback ({self.scrollback} righe) " header2 = f"║{title:^{self.term_cols - 4}}║" header3 = f"╚{'═' * (self.term_cols - 4)}╝" sys.stdout.write(f"\033[1;36m{header}\033[0m\n") sys.stdout.write(f"\033[1;36m{header2}\033[0m\n") sys.stdout.write(f"\033[1;36m{header3}\033[0m\n\n") # Mostra output buffer (solo le ultime righe che ci stanno) visible_lines = self.term_rows - 10 # Lascia spazio per prompt e status start_idx = max(0, len(self.output_buffer) - visible_lines) for line in self.output_buffer[start_idx:]: sys.stdout.write(line) # Status bar sys.stdout.write("\n") status_bar = f"\033[1;34m─" * (self.term_cols - 30) + f" Scrollback: {len(self.output_buffer)} righe\033[0m" sys.stdout.write(status_bar) # Prompt sys.stdout.write(f"\n\033[1;35m{self.prompt}\033[0m ") sys.stdout.flush() def _add_output(self, text: str, color: str = "32"): """Aggiunge output al buffer""" lines = text.split('\n') for line in lines: self.output_buffer.append(f"\033[1;{color}m{line}\033[0m\n") def start(self, agent_callback: Callable[[str], str]): """ Avvia il terminale interattivo. Args: agent_callback: Funzione che riceve l'input e ritorna la risposta """ self.agent_callback = agent_callback self.running = True # Registra signal handler per Ctrl+C def handle_sigint(signum, frame): sys.stdout.write("\n\033[1;33m👋 Arrivederci!\033[0m\n") sys.stdout.flush() self.running = False sys.exit(0) old_handler = signal.signal(signal.SIGINT, handle_sigint) try: # Mostra schermo iniziale self._redraw() while self.running: try: # Leggi input user_input = input() except EOFError: break except KeyboardInterrupt: continue if not user_input.strip(): continue if user_input.strip().lower() in ['/exit', '/quit']: sys.stdout.write("\033[1;33m👋 Arrivederci!\033[0m\n") break if user_input.strip() == '/clear': self.output_buffer.clear() self._redraw() continue if user_input.strip() == '/help': help_text = """ \033[1;36m╔══════════════════════════════════════════════════════╗\033[0m \033[1;36m║ COMANDI DISPONIBILI ║\033[0m \033[1;36m╠══════════════════════════════════════════════════════╣\033[0m \033[1;36m║ /help - Mostra questo messaggio ║\033[0m \033[1;36m║ /clear - Pulisci lo schermo ║\033[0m \033[1;36m║ /exit - Esci ║\033[0m \033[1;36m╚══════════════════════════════════════════════════════╝\033[0m """ self._add_output(help_text, "36") self._redraw() continue # Mostra comando utente self._add_output(f"👤 {user_input}", "36") self._redraw() # Mostra "thinking" self._add_output("🤔 Pensando...", "33") self._redraw() # Chiamata all'agent try: response = agent_callback(user_input) # Rimuovi "thinking" e mostra risposta if self.output_buffer and "🤔 Pensando..." in self.output_buffer[-1]: self.output_buffer.pop() self._add_output(response, "32") except Exception as e: self._add_output(f"❌ Errore: {str(e)}", "31") self._redraw() finally: signal.signal(signal.SIGINT, old_handler) def close(self): """Chiude il terminale""" self.running = False def create_terminal_ui( scrollback: int = 10000, prompt: str = "❯ " ) -> TerminalUI: """Factory function per creare TerminalUI""" return TerminalUI( scrollback=scrollback, prompt=prompt ) # Test standalone if __name__ == "__main__": def test_callback(text): return f"Ricevuto: {text}\n\nRisposta multi-linea:\n1. Prima\n2. Seconda\n3. Terza" print("Avvio TerminalUI di test...") term = create_terminal_ui() try: term.start(test_callback) except KeyboardInterrupt: print("\nUscito")