Files
langcode_agent/terminal_ui.py
T
opencode 20233db282 fix: riscrivi TerminalUI piu semplice e robusto
- Usa output_buffer per scrollback manuale
- Non usa pty/select complessi
- Piu stabile su terminali diversi
2026-03-24 17:26:55 +01:00

212 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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")