- Usa lista invece di bitwise OR per attributi terminale - Compatibilità con piu versioni Python/termios
251 lines
8.6 KiB
Python
251 lines
8.6 KiB
Python
"""
|
||
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)
|