From 39b9288773c59e50c0315c4e772841f2567b4233 Mon Sep 17 00:00:00 2001 From: opencode Date: Tue, 24 Mar 2026 17:34:06 +0100 Subject: [PATCH] feat: pyte UI con scrollback e navigazione PgUp/PgDn/frecce MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Modalità scroll: PgUp entra in scroll mode - q o ENTER esce da scroll mode - j/k frecce navigano nello scrollback - Resize automatico del terminale --- curses_ui.py | 293 +++++++++++++++++++++++++++++++++++ terminal_ui.py | 411 ++++++++++++++++++++++++++++++++++--------------- 2 files changed, 581 insertions(+), 123 deletions(-) create mode 100644 curses_ui.py diff --git a/curses_ui.py b/curses_ui.py new file mode 100644 index 0000000..dd15eb4 --- /dev/null +++ b/curses_ui.py @@ -0,0 +1,293 @@ +""" +Terminal UI con scrollback usando Curses +Scrollback vero con frecce e PgUp/PgDn +""" +import sys +import signal +import curses +from typing import Optional, Callable + +try: + import curses + CURSES_AVAILABLE = True +except ImportError: + CURSES_AVAILABLE = False + + +class CursesUI: + """ + Terminal UI con scrollback usando curses. + + Features: + - Scrollback completo con PgUp/PgDn + - Frecce su/giù per navigare + - Resize automatico + - Colors (se supportati) + """ + + def __init__( + self, + scrollback: int = 10000, + prompt: str = "❯ " + ): + """ + Inizializza il terminale UI con curses. + + Args: + scrollback: Numero di righe nello scrollback buffer + prompt: Prompt da mostrare + """ + if not CURSES_AVAILABLE: + raise ImportError("curses non è disponibile su questo sistema") + + self.scrollback = scrollback + self.prompt = prompt + + # Buffer per scrollback + self.lines: list = [] + + # Callback per l'agent + self.agent_callback: Optional[Callable[[str], str]] = None + + def _init_colors(self, stdscr): + """Inizializza i colori""" + if curses.has_colors(): + curses.start_color() + curses.use_default_colors() + # Colori: 1=cyan, 2=green, 3=yellow, 4=red, 5=magenta, 6=blue + curses.init_pair(1, curses.COLOR_CYAN, -1) # Header + curses.init_pair(2, curses.COLOR_GREEN, -1) # Agent output + curses.init_pair(3, curses.COLOR_YELLOW, -1) # Thinking + curses.init_pair(4, curses.COLOR_RED, -1) # Error + curses.init_pair(5, curses.COLOR_MAGENTA, -1) # Prompt + curses.init_pair(6, curses.COLOR_BLUE, -1) # User input + + def _add_line(self, text: str, color: int = 2): + """Aggiunge una linea al buffer""" + self.lines.append((text, color)) + # Limita la dimensione del buffer + if len(self.lines) > self.scrollback: + self.lines.pop(0) + + def _draw(self, stdscr): + """Disegna lo schermo""" + stdscr.clear() + + # Ottieni dimensioni + max_y, max_x = stdscr.getmaxyx() + + # Header + header = "═" * (max_x - 4) + stdscr.addstr(0, 0, f"╔{header}╗", curses.color_pair(1) | curses.A_BOLD) + title = " LangCode Agent - Curses UI " + stdscr.addstr(1, (max_x - len(title)) // 2, title, curses.color_pair(1) | curses.A_BOLD) + stdscr.addstr(2, 0, f"╚{header}╝", curses.color_pair(1) | curses.A_BOLD) + + # Calcola area visibile + start_row = 4 + visible_rows = max_y - start_row - 2 # -2 per status e prompt + + # Mostra linee visibili + total_lines = len(self.lines) + + # Scroll offset (0 = showing latest) + # La scrolling position va da 0 a max(0, total_lines - visible_rows) + self.scroll_offset = max(0, total_lines - visible_rows) + + # Disegna le linee visibili + for i in range(min(visible_rows, total_lines)): + line_idx = self.scroll_offset + i + if line_idx < total_lines: + text, color = self.lines[line_idx] + # Tronca se troppo lunga + display_text = text[:max_x - 2] if len(text) > max_x - 2 else text + try: + stdscr.addstr(start_row + i, 0, display_text, curses.color_pair(color)) + except curses.error: + pass + + # Status bar + status_y = max_y - 2 + scroll_info = f"Linee: {total_lines} | Scroll: {self.scroll_offset} | PgUp/PgDn=Frecce=Scroll | q=quit | Ctrl+C=exit" + stdscr.addstr(status_y, 0, "─" * (max_x - len(scroll_info) - 1), curses.A_DIM) + stdscr.addstr(status_y, max_x - len(scroll_info) - 1, scroll_info, curses.A_DIM) + + # Prompt + stdscr.addstr(max_y - 1, 0, f"\033[1;35m{self.prompt}\033[0m ", curses.color_pair(5) | curses.A_BOLD) + + stdscr.refresh() + + def _handle_input(self, stdscr): + """Gestisce l'input""" + curses.curs_set(1) # Mostra cursore + + # Abilita arrow keys e PgUp/PgDn + stdscr.keypad(True) + + # Configura timeout per read non-blocking + stdscr.timeout(100) # 100ms timeout + + input_buffer = "" + while True: + self._draw(stdscr) + + try: + key = stdscr.getch() + except curses.error: + continue + + if key == -1: + # Timeout - continua + continue + + elif key in [curses.KEY_ENTER, 10, 13]: + # Enter - processa comando + if input_buffer.strip(): + return input_buffer.strip() + input_buffer = "" + + elif key in [curses.KEY_UP]: + # Scroll up nel buffer + if self.scroll_offset > 0: + self.scroll_offset -= 1 + + elif key in [curses.KEY_DOWN]: + # Scroll down nel buffer + max_y, max_x = stdscr.getmaxyx() + visible_rows = max_y - 6 + if self.scroll_offset < max(0, len(self.lines) - visible_rows): + self.scroll_offset += 1 + + elif key in [curses.KEY_PPAGE]: + # PgUp - scroll su di una pagina + max_y, _ = stdscr.getmaxyx() + self.scroll_offset = max(0, self.scroll_offset - (max_y - 6)) + + elif key in [curses.KEY_NPAGE]: + # PgDn - scroll giu di una pagina + max_y, max_x = stdscr.getmaxyx() + visible_rows = max_y - 6 + self.scroll_offset = min(max(0, len(self.lines) - visible_rows), + self.scroll_offset + visible_rows) + + elif key == 27: # ESC + return "/exit" + + elif key in [3, 26]: # Ctrl+C + return "/exit" + + elif key in [curses.KEY_BACKSPACE, 127, 8]: + # Backspace + input_buffer = input_buffer[:-1] + + elif 32 <= key <= 126: + # Carattere stampabile + input_buffer += chr(key) + + # Mostra input corrente da qualche parte se necessario + + def start(self, agent_callback: Callable[[str], str]): + """ + Avvia il terminale interattivo con curses. + """ + self.agent_callback = agent_callback + + def handle_sigint(signum, frame): + sys.exit(0) + + old_handler = signal.signal(signal.SIGINT, handle_sigint) + + try: + curses.wrapper(self._main_loop) + finally: + signal.signal(signal.SIGINT, old_handler) + + def _main_loop(self, stdscr): + """Loop principale curses""" + self._init_colors(stdscr) + curses.cbreak() + curses.noecho() + + # Aggiungi header + self._add_line("╔═══════════════════════════════════════════════════════╗", 1) + self._add_line("║ LangCode Agent - Curses UI con Scrollback ║", 1) + self._add_line("╚═══════════════════════════════════════════════════════╝", 1) + self._add_line("", 0) + self._add_line("Scrivi un messaggio e premi Enter. Usa PgUp/PgDn per scrollare.", 6) + self._add_line("Comandi: /help, /clear, /exit", 6) + self._add_line("", 0) + + while True: + # Ottieni input + stdscr.timeout(-1) # Bloccante per l'input + command = self._handle_input(stdscr) + + if command.lower() in ['/exit', '/quit']: + break + + if not command: + continue + + if command == '/help': + self._add_line("╔═══════════════════════════════════════════════════════╗", 1) + self._add_line("║ COMANDI ║", 1) + self._add_line("╠═══════════════════════════════════════════════════════╣", 1) + self._add_line("║ /help - Mostra questo messaggio ║", 1) + self._add_line("║ /clear - Pulisci lo schermo ║", 1) + self._add_line("║ /exit - Esci dall'agente ║", 1) + self._add_line("║ PgUp - Scroll su ║", 1) + self._add_line("║ PgDn - Scroll giu ║", 1) + self._add_line("║ Frecce - Naviga nel scrollback ║", 1) + self._add_line("╚═══════════════════════════════════════════════════════╝", 1) + continue + + if command == '/clear': + self.lines.clear() + continue + + # Mostra comando utente + self._add_line("", 0) + self._add_line(f"👤 {command}", 6) + + # Chiamata all'agent + self._add_line("", 0) + self._add_line("🤔 Pensando...", 3) + + try: + response = agent_callback(command) + + # Rimuovi "thinking" e mostra risposta + if self.lines and "🤔 Pensando..." in self.lines[-1][0]: + self.lines.pop() + + # Aggiungi risposta + for line in response.split('\n'): + self._add_line(f"🤖 {line}", 2) + except Exception as e: + self._add_line(f"❌ Errore: {str(e)}", 4) + + self._add_line("", 0) + + +def create_curses_ui( + scrollback: int = 10000, + prompt: str = "❯ " +) -> CursesUI: + """Factory function per creare CursesUI""" + return CursesUI( + scrollback=scrollback, + prompt=prompt + ) + + +# Test standalone +if __name__ == "__main__": + def test_callback(text): + return f"Ricevuto: {text}\n\nRisposta multi-linea:\n1. Prima linea\n2. Seconda\n3. Terza\n4. Quarta\n5. Quinta" + + print("Avvio CursesUI di test...") + ui = create_curses_ui() + try: + ui.start(test_callback) + except KeyboardInterrupt: + print("\nUscito") diff --git a/terminal_ui.py b/terminal_ui.py index 52ac3cb..81ae35d 100644 --- a/terminal_ui.py +++ b/terminal_ui.py @@ -1,6 +1,6 @@ """ Terminal UI con scrollback usando Pyte -Versione semplificata e robusta +Versione con navigazione scrollback """ import os import sys @@ -20,9 +20,9 @@ class TerminalUI: Terminal emulator con scrollback usando pyte. Features: - - Scrollback buffer completo (10000 righe) - - Output colorato con ANSI - - Resize del terminale + - Scrollback buffer completo + - Navigazione con frecce e PgUp/PgDn + - Output colorato """ def __init__( @@ -30,160 +30,331 @@ class TerminalUI: 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") + raise ImportError("pyte non è installato") 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)) + self.rows, self.cols = shutil.get_terminal_size() + self.rows = max(24, min(self.rows, 50)) + self.cols = max(80, min(self.cols, 120)) - # Crea lo screen pyte - self.screen = pyte.Screen(self.term_cols, self.term_rows) + # Crea screen pyte + self.screen = pyte.Screen(self.cols, self.rows) self.screen.scroll_history = scrollback + + # Stream per feed input self.stream = pyte.Stream(self.screen) - # Buffer per l'output completo (per scrollback) - self.output_buffer: list = [] + # Scroll offset (0 = latest) + self.scroll_offset = 0 - # Flag per uscire + # Modalità input: "input" o "scroll" + self.mode = "input" + + # Linea di input corrente + self.input_buffer = "" + self.cursor_pos = 0 + + # Flag uscita self.running = False - # Callback per l'agent + # Callback agent self.agent_callback: Optional[Callable[[str], str]] = None + + # Signal handler + self._old_sigint: Optional[signal.Handler] = None - def _redraw(self): + def _draw(self): """Ridisegna lo schermo""" - # Clear schermo + # Clear 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)}╝" + # Header con colori + header_color = "\033[1;36m" + reset = "\033[0m" - 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") + header_line = "═" * (self.cols - 4) + sys.stdout.write(f"{header_color}╔{header_line}╗{reset}\n") + title = " LangCode Agent - pyte UI " + title_pos = (self.cols - len(title)) // 2 + sys.stdout.write(f"{header_color}║{reset}" + " " * title_pos + f"\033[1;33m{title}\033[0m" + " " * (self.cols - title_pos - len(title) - 3) + f"{header_color}║{reset}\n") + sys.stdout.write(f"{header_color}╚{header_line}╝{reset}\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) + # Contenuto scrollback + # Lo screen.display contiene le linee visibili + # Ma dobbiamo mostrare anche lo scroll_history - for line in self.output_buffer[start_idx:]: - sys.stdout.write(line) + if self.mode == "scroll" or self.scroll_offset > 0: + # Mostra modalità scroll + visible_content = self._get_visible_content() + for i, line in enumerate(visible_content): + # Tronca o pad la linea + display = line[:self.cols - 2] if len(line) > self.cols - 2 else line + sys.stdout.write(display + " " * (self.cols - len(display) - 1) + "\n") + else: + # Modalità input normale - mostra output recente + # Lo screen.display ha le ultime self.rows linee + for line in self.screen.display: + # Rimuovi escape codes per display puro + clean = self._strip_ansi(line.rstrip()) + if clean: # Solo linee non vuote + sys.stdout.write(clean + "\n") # 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) + status_color = "\033[1;34m" + scroll_info = f" Scrollback: {len(self.screen.scroll_history)} linee | Modalità: {self.mode} " + if self.mode == "scroll": + scroll_info += f" | Offset: {self.scroll_offset} | q=exit scroll | ENTER=exit " + else: + scroll_info += f" | PgUp=scroll | q=quit " + sys.stdout.write(f"\n{status_color}{'─' * (self.cols - len(scroll_info) - 1)}{reset}{scroll_info}{reset}\n") + + # Input line o prompt + if self.mode == "input": + sys.stdout.write(f"\033[1;35m{self.prompt}\033[0m {self.input_buffer}") + # Move cursor back if needed + if self.cursor_pos < len(self.input_buffer): + move_back = len(self.input_buffer) - self.cursor_pos + sys.stdout.write(f"\033[{move_back}D") + else: + sys.stdout.write(f"\033[1;33m[SCROLL MODE] Premi q o ENTER per uscire dallo scroll\033[0m") - # 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 _get_visible_content(self) -> list: + """Ottieni contenuto visibile considerando lo scroll""" + all_lines = list(self.screen.scroll_history) + list(self.screen.display) + + if not all_lines: + return [] + + # Calcola offset dal fondo + total = len(all_lines) + visible = self.rows - 8 # -header, status, prompt, etc. + + start = max(0, total - self.scroll_offset - visible) + end = total - self.scroll_offset + + if end <= start: + end = start + visible + + return all_lines[start:end] + + def _strip_ansi(self, text: str) -> str: + """Rimuovi codici ANSI da una stringa""" + import re + ansi_pattern = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') + return ansi_pattern.sub('', text) + + def _handle_key(self, char: str): + """Gestisce un singolo carattere input""" + if char == '\x1b': # ESC + self.mode = "scroll" + self.scroll_offset = len(self.screen.scroll_history) + len(self.screen.display) - self.rows + 5 + return + + if self.mode == "scroll": + if char in ['q', 'Q']: + self.mode = "input" + self.scroll_offset = 0 + elif char == '\r' or char == '\n': + self.mode = "input" + self.scroll_offset = 0 + elif char in ['\x7f', '\x08']: # Backspace in scroll mode + self.scroll_offset = max(0, self.scroll_offset - 1) + elif char == 'j': # vim-style down + self.scroll_offset = max(0, self.scroll_offset - 1) + elif char == 'k': # vim-style up + self.scroll_offset += 1 + return + + # Modalità input + if char == '\r' or char == '\n': + # Processa comando + cmd = self.input_buffer + self.input_buffer = "" + self.cursor_pos = 0 + self.stream.feed('\r\n') + return cmd + elif char == '\x7f' or char == '\x08': # Backspace + if 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.stream.feed('\b \b') + elif char == '\x03': # Ctrl+C + return None + elif char == '\x0c': # Ctrl+L (clear) + self.screen.reset() + self.input_buffer = "" + self.cursor_pos = 0 + else: + # Carattere normale + if len(char) == 1 and ord(char) >= 32 and ord(char) <= 126: + self.input_buffer = self.input_buffer[:self.cursor_pos] + char + self.input_buffer[self.cursor_pos:] + self.cursor_pos += 1 + self.stream.feed(char) + + return None + + def _process_command(self, command: str): + """Processa un comando""" + if not command.strip(): + return + + # Output comando utente + prompt_color = "\033[1;36m" + reset = "\033[0m" + self.stream.feed(f'\r\n{prompt_color}👤 {command}{reset}\r\n\n') + + # Thinking indicator + thinking_color = "\033[1;33m" + self.stream.feed(f'{thinking_color}🤔 Pensando...{reset}\r\n') + + # Chiamata agent + try: + response = self.agent_callback(command) + + # Rimuovi thinking e mostra risposta + # Scroll back di una linea + if len(self.screen.scroll_history) > 0: + # Il pensiero è nell'ultima linea visibile, rimuoviamolo + pass + + # Output risposta + for line in response.split('\n'): + self.stream.feed(f'\r\n\033[1;32m🤖 {line}\033[0m') + + self.stream.feed('\r\n') + + except Exception as e: + self.stream.feed(f'\r\n\033[1;31m❌ Errore: {str(e)}\033[0m\r\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 - """ + """Avvia il terminale interattivo""" self.agent_callback = agent_callback self.running = True - # Registra signal handler per Ctrl+C + # Signal handler 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) + self._old_sigint = signal.signal(signal.SIGINT, handle_sigint) try: - # Mostra schermo iniziale - self._redraw() + # Abilita raw mode per catturare singoli tasti + import tty + import termios - while self.running: - try: - # Leggi input - user_input = input() - except EOFError: - break - except KeyboardInterrupt: - continue + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + + try: + # Set raw mode + tty.setraw(fd) - if not user_input.strip(): - continue + # Mostra schermo iniziale + self._draw() - 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) + while self.running: + # Leggi un carattere + ch = os.read(fd, 1) - # Rimuovi "thinking" e mostra risposta - if self.output_buffer and "🤔 Pensando..." in self.output_buffer[-1]: - self.output_buffer.pop() + if not ch: + continue - self._add_output(response, "32") - except Exception as e: - self._add_output(f"❌ Errore: {str(e)}", "31") - - self._redraw() + char = ch.decode('utf-8', errors='replace') + + # Gestisci PgUp/PgDn in raw mode + if char == '\x1b': # ESC sequence start + # Leggi il resto della sequenza + 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 = max(0, len(self.screen.scroll_history) + len(self.screen.display) - self.rows + 5) + self._draw() + continue + elif seq == '[6~': # PgDn + if self.mode == "scroll": + self.scroll_offset = max(0, self.scroll_offset - (self.rows - 8)) + if self.scroll_offset <= 0: + self.mode = "input" + self._draw() + continue + elif seq == '[A': # Up arrow + if self.mode == "scroll": + self.scroll_offset += 1 + self._draw() + continue + elif seq == '[B': # Down arrow + if self.mode == "scroll": + self.scroll_offset = max(0, self.scroll_offset - 1) + self._draw() + continue + + # Processa il carattere + result = self._handle_key(char) + + if result is None: + self._draw() + continue + + if result == "": + self._draw() + continue + + if result is None and char == '\x03': # Ctrl+C + break + + # Esegui comando + if result: + if result.lower() == '/exit': + break + elif result.lower() == '/help': + self.stream.feed('\r\n') + help_lines = [ + "\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 schermo ║\033[0m", + "\033[1;36m║ /exit - Esci ║\033[0m", + "\033[1;36m║ PgUp - Entra in modalità scroll ║\033[0m", + "\033[1;36m║ q - Exit modalità scroll ║\033[0m", + "\033[1;36m╚══════════════════════════════════════════════╝\033[0m", + ] + for line in help_lines: + self.stream.feed(line + '\r\n') + elif result.lower() == '/clear': + self.screen.reset() + self.input_buffer = "" + self.cursor_pos = 0 + else: + self._process_command(result) + + self._draw() + + finally: + # Ripristina terminale + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + except Exception as e: + print(f"Errore: {e}") + import traceback + traceback.print_exc() finally: - signal.signal(signal.SIGINT, old_handler) + sys.stdout.write("\n") + sys.stdout.flush() def close(self): - """Chiude il terminale""" + """Chiudi il terminale""" self.running = False @@ -191,21 +362,15 @@ def create_terminal_ui( scrollback: int = 10000, prompt: str = "❯ " ) -> TerminalUI: - """Factory function per creare TerminalUI""" - return TerminalUI( - scrollback=scrollback, - prompt=prompt - ) + """Factory function""" + return TerminalUI(scrollback=scrollback, prompt=prompt) -# Test standalone +# Test if __name__ == "__main__": def test_callback(text): - return f"Ricevuto: {text}\n\nRisposta multi-linea:\n1. Prima\n2. Seconda\n3. Terza" + return f"Ricevuto: {text}\n\nLunga risposta:\n" + "\n".join([f"Linea {i}" for i in range(1, 21)]) - print("Avvio TerminalUI di test...") - term = create_terminal_ui() - try: - term.start(test_callback) - except KeyboardInterrupt: - print("\nUscito") + print("Avvio TerminalUI...") + ui = create_terminal_ui() + ui.start(test_callback)