""" Terminal UI con scrollback semplice Standard library only, super compatibile """ import os import sys import signal import fcntl import struct import termios from typing import Optional, Callable class TerminalUI: """ Terminal UI con scrollback semplice. """ def __init__(self, scrollback: int = 10000, prompt: str = "❯ "): self.scrollback = scrollback self.prompt = prompt # Dimensioni terminale - rileva in tempo reale self.rows = 24 self.cols = 80 self._update_size() # Buffer scrollback self.history: list = [] # Scroll offset self.scroll_offset = 0 self.mode = "input" # Input self.input_buffer = "" self.cursor_pos = 0 # Callback self.agent_callback: Optional[Callable[[str], str]] = None def _update_size(self): """Rileva dimensioni terminale""" try: winsize = struct.pack('HHHH', 0, 0, 0, 0) winsize = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, winsize) self.rows, self.cols = struct.unpack('HHHH', winsize)[:2] except: self.rows, self.cols = 24, 80 self.rows = max(20, min(self.rows, 50)) self.cols = max(40, min(self.cols, 120)) def _draw(self): """Disegna schermo""" reset = "\033[0m" cyan = "\033[1;36m" yellow = "\033[1;33m" blue = "\033[1;34m" green = "\033[1;32m" # Aggiorna dimensioni self._update_size() # Clear sys.stdout.write("\033[2J\033[H") # Header semplice sys.stdout.write(f"{cyan}=== LangCode Agent ==={reset}\n\n") # Contenuto - righe visibili visible = self.rows - 6 total = len(self.history) if self.mode == "scroll": start = max(0, total - self.scroll_offset - visible) else: start = max(0, total - visible) for i in range(start, min(total, start + visible)): text, _ = self.history[i] # Stampa linea pulita sys.stdout.write(text[:self.cols]) sys.stdout.write("\n") # Status bar sys.stdout.write(f"\n{blue}--- INPUT | Lines: {total} | PgUp=scroll | q=quit ---{reset}\n") # Prompt if self.mode == "input": sys.stdout.write(f"{cyan}{self.prompt}{reset} {self.input_buffer}") # reposiziona cursore se necessario if self.cursor_pos < len(self.input_buffer): back = len(self.input_buffer) - self.cursor_pos sys.stdout.write(f"\033[{back}D") sys.stdout.flush() def _add_history(self, text: str, color: str = ""): """Aggiunge a history""" self.history.append((text, color)) while len(self.history) > self.scrollback: self.history.pop(0) def _process(self, cmd: str): """Processa comando""" if not cmd.strip(): return self._add_history(f"👤 {cmd}", "") # Thinking self._add_history("🤔 Pensando...", "") self._draw() try: resp = self.agent_callback(cmd) # Rimuovi thinking if self.history and "🤔" in self.history[-1][0]: self.history.pop() # Aggiungi risposta for line in resp.split('\n'): if line.strip(): self._add_history(f"🤖 {line}", "") except Exception as e: if self.history and "🤔" in self.history[-1][0]: self.history.pop() self._add_history(f"❌ {str(e)}", "") def start(self, agent_callback: Callable[[str], str]): """Avvia UI""" self.agent_callback = agent_callback def sigint_handler(signum, frame): sys.stdout.write("\n\033[1;33m👋 Arrivederci!\033[0m\n") sys.exit(0) signal.signal(signal.SIGINT, sigint_handler) try: # Raw mode fd = sys.stdin.fileno() old = termios.tcgetattr(fd) # nuovo = disabilita ICANON e ECHO new = list(old) new[3] = new[3] & ~(termios.ICANON | termios.ECHO) termios.tcsetattr(fd, termios.TCSADRAIN, new) self._draw() while True: ch = os.read(fd, 1) if not ch: continue char = ch.decode('utf-8', errors='replace') # ESC sequence if char == '\x1b': import select if select.select([fd], [], [], 0.1)[0]: seq = os.read(fd, 2).decode('utf-8', errors='replace') if seq == '[5~': # PgUp self.mode = "scroll" self.scroll_offset = len(self.history) self._draw() elif seq == '[6~': # PgDn if self.mode == "scroll": self.scroll_offset = max(0, self.scroll_offset - 5) if self.scroll_offset <= 0: self.mode = "input" self._draw() elif seq == '[A': # Up if self.mode == "scroll": self.scroll_offset = min(len(self.history), self.scroll_offset + 1) self._draw() elif seq == '[B': # Down if self.mode == "scroll": self.scroll_offset = max(0, self.scroll_offset - 1) self._draw() continue # Enter if char == '\n' or char == '\r': if self.mode == "scroll": self.mode = "input" self.scroll_offset = 0 self._draw() continue cmd = self.input_buffer self.input_buffer = "" self.cursor_pos = 0 if cmd.lower() in ['/exit', '/quit']: break elif cmd.lower() == '/help': for line in [ "=== COMANDI ===", "/help - Questo messaggio", "/clear - Pulisci schermo", "/exit - Esci", "PgUp - Modalita scroll", "q - Esci da scroll", ]: self._add_history(line, "") elif cmd.lower() == '/clear': self.history.clear() elif cmd: self._process(cmd) self._draw() continue # Backspace if char == '\x7f' or char == '\x08': if self.mode == "input" and self.cursor_pos > 0: self.input_buffer = self.input_buffer[:self.cursor_pos-1] + self.input_buffer[self.cursor_pos:] self.cursor_pos -= 1 self._draw() continue # q in scroll mode if char == 'q' and self.mode == "scroll": self.mode = "input" self.scroll_offset = 0 self._draw() continue # Caratteri stampabili if 32 <= ord(char) <= 126: if self.mode == "input": self.input_buffer = self.input_buffer[:self.cursor_pos] + char + self.input_buffer[self.cursor_pos:] self.cursor_pos += 1 self._draw() finally: termios.tcsetattr(fd, termios.TCSADRAIN, old) sys.stdout.write("\n") def create_terminal_ui(scrollback: int = 10000, prompt: str = "❯ ") -> TerminalUI: return TerminalUI(scrollback=scrollback, prompt=prompt) if __name__ == "__main__": def test(text): return f"Ricevuto: {text}\n" + "\n".join([f"Linea {i}" for i in range(1, 21)]) ui = create_terminal_ui() ui.start(test)