Files
langcode_agent/terminal_ui.py
T
opencode 858b469def fix: correggi raw mode per terminali che non supportano bitwise OR
- Usa lista invece di bitwise OR per attributi terminale
- Compatibilità con piu versioni Python/termios
2026-03-24 18:42:54 +01:00

251 lines
8.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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)