refactor: UI semplicissima con input() standard

- input() normale invece di raw mode
- /scroll per vedere tutto lo scrollback con paginazione
- /history per vedere ultime 50 righe
- Molto piu stabile e compatibile
- Niente piu problemi di escape sequences
This commit is contained in:
opencode committed 2026-03-24 18:44:55 +01:00
1 parent 858b469def
commit 442c91ee66
1 file changed
+161 -187
+161 -187
View File
@@ -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)