- Usa output_buffer per scrollback manuale - Non usa pty/select complessi - Piu stabile su terminali diversi
212 lines
7.1 KiB
Python
212 lines
7.1 KiB
Python
"""
|
||
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")
|