Files
langcode_agent/terminal_ui.py
T
opencode aec5736394 fix: wrap linee e padding per display corretto
- Testo wrap a colonne del terminale
- Padding con spazi per linee corte
- Rimosso riferimento a pyte non usato
2026-03-24 18:07:22 +01:00

340 lines
12 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
Niente dipendenze esterne, solo standard library
"""
import os
import sys
import signal
import shutil
from typing import Optional, Callable
class TerminalUI:
"""
Terminal UI con scrollback semplice.
Features:
- Scrollback buffer
- Navigazione con frecce e PgUp/PgDn
- Output colorato
"""
def __init__(
self,
scrollback: int = 10000,
prompt: str = "❯ "
):
self.scrollback = scrollback
self.prompt = prompt
# Dimensioni terminale
self.rows, self.cols = shutil.get_terminal_size()
self.rows = max(24, min(self.rows, 50))
self.cols = max(80, min(self.cols, 120))
# Buffer scrollback
self.history: list = []
# Scroll offset (0 = latest)
self.scroll_offset = 0
# Modalità: "input" o "scroll"
self.mode = "input"
# Input buffer
self.input_buffer = ""
self.cursor_pos = 0
# Flag uscita
self.running = False
# Callback agent
self.agent_callback: Optional[Callable[[str], str]] = None
def _add_to_history(self, text: str, color: str = "0"):
"""Aggiunge linea allo history (wrap a cols)"""
import re
# Rimuovi codes ANSI per calcolare lunghezza reale
ansi_pattern = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
clean = ansi_pattern.sub('', text)
# Wrap del testo se troppo lungo
max_width = self.cols - 2
lines = []
while len(clean) > max_width:
lines.append(clean[:max_width])
clean = clean[max_width:]
if clean:
lines.append(clean)
# Aggiungi tutte le linee
for line in lines:
self.history.append((line, color))
# Limita history
while len(self.history) > self.scrollback:
self.history.pop(0)
def _draw(self):
"""Ridisegna lo schermo"""
reset = "\033[0m"
# Clear screen
sys.stdout.write("\033[2J\033[H")
# Header
header_color = "\033[1;36m"
yellow = "\033[1;33m"
line = "═" * (self.cols - 4)
sys.stdout.write(f"{header_color}╔{line}╗{reset}\n")
title = " LangCode Agent "
title_pos = (self.cols - len(title)) // 2
sys.stdout.write(f"{header_color}║{reset}" + " " * title_pos + f"{yellow}{title}{reset}" + " " * (self.cols - title_pos - len(title) - 3) + f"{header_color}║{reset}\n")
sys.stdout.write(f"{header_color}╚{line}╝{reset}\n")
# Contenuto
visible = self.rows - 7
if self.mode == "scroll":
# Mostra scrollback
total = len(self.history)
start = max(0, total - self.scroll_offset - visible)
end = min(total, start + visible)
for i in range(start, end):
text, color = self.history[i]
# Pad con spazi se necessario
display = text.ljust(self.cols - 1)
sys.stdout.write(f"\033[{color}m{display}{reset}\n")
else:
# Modalità input - mostra ultime N linee
total = len(self.history)
start = max(0, total - visible)
for i in range(start, total):
text, color = self.history[i]
display = text.ljust(self.cols - 1)
sys.stdout.write(f"\033[{color}m{display}{reset}\n")
# Status bar
blue = "\033[1;34m"
dim = "\033[2m"
total_lines = len(self.history)
mode_str = "SCROLL" if self.mode == "scroll" else "INPUT"
scroll_info = f" {mode_str} | Lines: {total_lines} "
if self.mode == "scroll":
scroll_info += f"| Offset: {self.scroll_offset} | q/ENTER=exit scroll | arrows/jk=scroll "
else:
scroll_info += "| PgUp=scroll | q=quit "
status_width = self.cols - len(scroll_info) - 1
sys.stdout.write(f"{blue}{'─' * status_width}{reset}{blue}{scroll_info}{reset}\n")
# Input line
if self.mode == "input":
sys.stdout.write(f"\033[1;35m{self.prompt}\033[0m {self.input_buffer}")
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] q o ENTER per uscire dallo scroll{reset}")
sys.stdout.flush()
def _handle_key(self, char: str):
"""Gestisce un carattere input"""
# ESC sequence
if char == '\x1b':
self.mode = "scroll"
self.scroll_offset = len(self.history)
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 ['j', 'J', '\'', '\"']: # down
self.scroll_offset = min(len(self.history), self.scroll_offset + 1)
elif char in ['k', 'K']: # up
self.scroll_offset = max(0, self.scroll_offset - 1)
return
# Input mode
if char == '\r' or char == '\n':
cmd = self.input_buffer
self.input_buffer = ""
self.cursor_pos = 0
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
elif char == '\x03': # Ctrl+C
return None
elif char == '\x0c': # Ctrl+L
self.history.clear()
self.input_buffer = ""
self.cursor_pos = 0
else:
if len(char) == 1 and 32 <= ord(char) <= 126:
self.input_buffer = self.input_buffer[:self.cursor_pos] + char + self.input_buffer[self.cursor_pos:]
self.cursor_pos += 1
return None
def _process_command(self, command: str):
"""Processa un comando"""
if not command.strip():
return
# Aggiungi comando
self._add_to_history(f"👤 {command}", "36")
# Thinking
self._add_to_history("🤔 Pensando...", "33")
self._draw()
try:
response = self.agent_callback(command)
# Rimuovi thinking
if self.history and "🤔" in self.history[-1][0]:
self.history.pop()
# Aggiungi risposta
for line in response.split('\n'):
if line.strip():
self._add_to_history(f"🤖 {line}", "32")
except Exception as e:
if self.history and "🤔" in self.history[-1][0]:
self.history.pop()
self._add_to_history(f"❌ Errore: {str(e)}", "31")
def start(self, agent_callback: Callable[[str], str]):
"""Avvia il terminale"""
self.agent_callback = agent_callback
self.running = True
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)
signal.signal(signal.SIGINT, handle_sigint)
try:
import tty
import termios
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(fd)
self._draw()
while self.running:
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()
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
if self.mode == "scroll":
self.scroll_offset = min(len(self.history), self.scroll_offset + 1)
self._draw()
continue
elif seq == '[B': # Down
if self.mode == "scroll":
self.scroll_offset = max(0, self.scroll_offset - 1)
self._draw()
continue
result = self._handle_key(char)
self._draw()
if result is not None and result != "":
if result.lower() == '/exit':
break
elif result.lower() == '/help':
help_text = [
("╔══════════════════════════════════════╗", "36"),
("║ COMANDI DISPONIBILI ║", "36"),
("╠══════════════════════════════════════╣", "36"),
("║ /help - Mostra questo messaggio ║", "36"),
("║ /clear - Pulisci schermo ║", "36"),
("║ /exit - Esci ║", "36"),
("║ PgUp - Modalità scroll ║", "36"),
("║ q - Exit modalità scroll ║", "36"),
("╚══════════════════════════════════════╝", "36"),
]
for text, color in help_text:
self._add_to_history(text, color)
elif result.lower() == '/clear':
self.history.clear()
else:
self._process_command(result)
self._draw()
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
except Exception as e:
print(f"Errore: {e}")
import traceback
traceback.print_exc()
finally:
sys.stdout.write("\n")
sys.stdout.flush()
def close(self):
self.running = False
def create_terminal_ui(
scrollback: int = 10000,
prompt: str = "❯ "
) -> TerminalUI:
"""Factory function"""
return TerminalUI(scrollback=scrollback, prompt=prompt)
# Test
if __name__ == "__main__":
def test_callback(text):
lines = [f"Ricevuto: {text}"]
lines += [f"Linea {i}" for i in range(1, 21)]
return "\n".join(lines)
print("Avvio TerminalUI...")
ui = create_terminal_ui()
ui.start(test_callback)