diff --git a/terminal_ui.py b/terminal_ui.py index 78153c7..2154ba0 100644 --- a/terminal_ui.py +++ b/terminal_ui.py @@ -1,241 +1,213 @@ """ Terminal UI con scrollback semplice -Standard library only, super compatibile +Usa input() standard + visualizzazione scrollback """ import os import sys import signal -import fcntl -import struct -import termios from typing import Optional, Callable class TerminalUI: """ - Terminal UI con scrollback semplice. + Terminal UI con scrollback. + Uso semplice: input() per input, history buffer per scrollback. """ 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 + # History buffer 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) + def _print(self, text: str): + """Stampa con colori base""" + # Semplice - solo reset e bold + if text.startswith("πŸ‘€"): + print(f"\033[1;36m{text}\033[0m") + elif text.startswith("πŸ€–"): + print(f"\033[1;32m{text}\033[0m") + elif text.startswith("πŸ€”"): + print(f"\033[1;33m{text}\033[0m") + elif text.startswith("❌"): + print(f"\033[1;31m{text}\033[0m") 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() + print(text) - def _add_history(self, text: str, color: str = ""): + def _wrap_line(self, text: str, width: int = 80) -> list: + """Wrap linea a width""" + lines = [] + while len(text) > width: + lines.append(text[:width]) + text = text[width:] + if text: + lines.append(text) + return lines + + def _add_history(self, text: str): """Aggiunge a history""" - self.history.append((text, color)) + self.history.append(text) while len(self.history) > self.scrollback: self.history.pop(0) + def _show_history(self, lines: int = None): + """Mostra ultime N linee di history""" + if lines is None: + # Determina righe visibili + try: + size = os.get_terminal_size() + lines = size.lines - 5 # spazio per header, status, prompt + except: + lines = 20 + + # Mostra ultime righe + start = max(0, len(self.history) - lines) + for line in self.history[start:]: + self._print(line) + def _process(self, cmd: str): """Processa comando""" if not cmd.strip(): return - self._add_history(f"πŸ‘€ {cmd}", "") + self._add_history(f"πŸ‘€ {cmd}") # Thinking - self._add_history("πŸ€” Pensando...", "") - self._draw() + print("\033[1;33mπŸ€” Pensando...\033[0m") try: resp = self.agent_callback(cmd) - # Rimuovi thinking - if self.history and "πŸ€”" in self.history[-1][0]: - self.history.pop() - # Aggiungi risposta + + # Rimuovi thinking (print sopra con risposta) + print("\033[2A\033[K", end="") # move up 2, clear line + + # Stampa risposta for line in resp.split('\n'): if line.strip(): - self._add_history(f"πŸ€– {line}", "") + self._add_history(f"πŸ€– {line}") + self._print(f"πŸ€– {line}") + except Exception as e: - if self.history and "πŸ€”" in self.history[-1][0]: - self.history.pop() - self._add_history(f"❌ {str(e)}", "") + print("\033[2A\033[K", end="") + err_msg = f"❌ {str(e)}" + self._add_history(err_msg) + self._print(err_msg) + + def _scroll_help(self): + """Mostra aiuto scroll""" + print("\n" + "=" * 60) + print("SCROLLBACK - Premi q per uscire, frecce per navigare") + print("=" * 60) + print() + + # Paginator semplice + page_size = 30 + start = 0 + total = len(self.history) + + while start < total: + # Mostra pagina + for i in range(start, min(start + page_size, total)): + self._print(self.history[i]) + + start += page_size + + if start < total: + print() + print(f"[{start}/{total} righe] - Premi ENTER per continuare, q per uscire") + try: + choice = input().strip() + if choice.lower() == 'q': + break + except: + break + + print() + print("=" * 60) + print("FINE SCROLLBACK") + print("=" * 60) def start(self, agent_callback: Callable[[str], str]): """Avvia UI""" self.agent_callback = agent_callback + # Signal handler def sigint_handler(signum, frame): - sys.stdout.write("\n\033[1;33mπŸ‘‹ Arrivederci!\033[0m\n") + print("\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() + # Header + print() + print("\033[1;36m" + "=" * 50 + "\033[0m") + print("\033[1;36m=\033[0m" + " " * 15 + "\033[1;33mLangCode Agent\033[0m" + " " * 15 + "\033[1;36m=\033[0m") + print("\033[1;36m" + "=" * 50 + "\033[0m") + print() + print("digita /help per aiuto, /exit per uscire") + print() - finally: - termios.tcsetattr(fd, termios.TCSADRAIN, old) - sys.stdout.write("\n") + while True: + try: + # Input + user_input = input(f"\033[1;35m{self.prompt}\033[0m ").strip() + except EOFError: + break + except KeyboardInterrupt: + print("\n\033[1;33mπŸ‘‹ Arrivederci!\033[0m") + break + + if not user_input: + continue + + # Comandi speciali + if user_input.lower() == '/exit': + print("\033[1;33mπŸ‘‹ Arrivederci!\033[0m") + break + + elif user_input.lower() == '/quit': + print("\033[1;33mπŸ‘‹ Arrivederci!\033[0m") + break + + elif user_input.lower() == '/help': + print() + print("\033[1;36m" + "=" * 40 + "\033[0m") + print("COMANDI:") + print(" /help - Questo messaggio") + print(" /clear - Pulisci schermo") + print(" /history - Mostra cronologia") + print(" /scroll - Entra in scrollback") + print(" /exit, /quit - Esci") + print() + print("SCROLLBACK:") + print(" /scroll - Mostra tutto lo scrollback") + print(" Frecce su/giΓΉ - Naviga cronologia (in some terminals)") + print() + print("\033[1;36m" + "=" * 40 + "\033[0m") + print() + continue + + elif user_input.lower() == '/clear': + os.system('clear' if os.name != 'nt' else 'cls') + continue + + elif user_input.lower() == '/history': + print() + self._show_history(50) + print() + continue + + elif user_input.lower() == '/scroll': + self._scroll_help() + continue + + # Processa comando normale + self._process(user_input) + print() def create_terminal_ui(scrollback: int = 10000, prompt: str = "❯ ") -> TerminalUI: @@ -244,7 +216,9 @@ def create_terminal_ui(scrollback: int = 10000, prompt: str = "❯ ") -> Termina if __name__ == "__main__": def test(text): - return f"Ricevuto: {text}\n" + "\n".join([f"Linea {i}" for i in range(1, 21)]) + lines = [f"Ricevuto: {text}"] + lines += [f"Linea {i}: contenuto di esempio" for i in range(1, 31)] + return "\n".join(lines) ui = create_terminal_ui() ui.start(test)