- Rimosso CursorShape.BEAM che causa hang su alcuni terminali - Aggiunto timeout 30s con fallback a CLI semplice - Cattura KeyboardInterrupt, EOFError, OSError per hang - Signal handler per Ctrl+C in EnhancedCLI
735 lines
28 KiB
Python
735 lines
28 KiB
Python
"""
|
||
Enhanced CLI for LangCode Agent
|
||
Provides modern terminal UI with colors, history, autocomplete, and more.
|
||
"""
|
||
|
||
import sys
|
||
import os
|
||
import re
|
||
import shutil
|
||
from typing import Optional, List, Dict, Any, Callable
|
||
from datetime import datetime
|
||
|
||
# prompt_toolkit imports
|
||
from prompt_toolkit import PromptSession, HTML
|
||
from prompt_toolkit.key_binding import KeyBindings
|
||
from prompt_toolkit.styles import Style
|
||
from prompt_toolkit.completion import WordCompleter, NestedCompleter, PathCompleter
|
||
from prompt_toolkit.history import FileHistory
|
||
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
||
from prompt_toolkit.formatted_text import FormattedText
|
||
from prompt_toolkit.cursor_shapes import CursorShape
|
||
|
||
# For syntax highlighting
|
||
try:
|
||
from pygments.lexers import get_lexer_by_name, get_lexer_for_filename
|
||
from pygments.token import Token
|
||
from pygments import highlight as pygments_highlight
|
||
PYGMENTS_AVAILABLE = True
|
||
except ImportError:
|
||
PYGMENTS_AVAILABLE = False
|
||
get_lexer_by_name = None
|
||
get_lexer_for_filename = None
|
||
pygments_highlight = None
|
||
|
||
|
||
# =============================================================================
|
||
# STYLING
|
||
# =============================================================================
|
||
|
||
class Colors:
|
||
"""ANSI color codes for terminal output"""
|
||
RESET = "\033[0m"
|
||
BOLD = "\033[1m"
|
||
DIM = "\033[2m"
|
||
ITALIC = "\033[3m"
|
||
|
||
BLACK = "\033[30m"
|
||
RED = "\033[31m"
|
||
GREEN = "\033[32m"
|
||
YELLOW = "\033[33m"
|
||
BLUE = "\033[34m"
|
||
MAGENTA = "\033[35m"
|
||
CYAN = "\033[36m"
|
||
WHITE = "\033[37m"
|
||
|
||
BRIGHT_BLACK = "\033[90m"
|
||
BRIGHT_RED = "\033[91m"
|
||
BRIGHT_GREEN = "\033[92m"
|
||
BRIGHT_YELLOW = "\033[93m"
|
||
BRIGHT_BLUE = "\033[94m"
|
||
BRIGHT_MAGENTA = "\033[95m"
|
||
BRIGHT_CYAN = "\033[96m"
|
||
BRIGHT_WHITE = "\033[97m"
|
||
|
||
|
||
class Theme:
|
||
"""Color themes for the CLI"""
|
||
|
||
@staticmethod
|
||
def get_style(theme_name: str) -> Style:
|
||
"""Get prompt_toolkit style from theme name"""
|
||
themes = {
|
||
"default": {
|
||
"user": "ansicyan bold",
|
||
"agent": "ansigreen",
|
||
"system": "ansiyellow",
|
||
"error": "ansired bold",
|
||
"info": "ansiblue",
|
||
"muted": "ansiblack",
|
||
"prompt": "ansimagenta bold",
|
||
"code": "ansiyellow",
|
||
"url": "ansiblue underline",
|
||
},
|
||
"dark": {
|
||
"user": "cyan bold",
|
||
"agent": "lightgreen",
|
||
"system": "yellow",
|
||
"error": "lightred bold",
|
||
"info": "lightblue",
|
||
"muted": "gray",
|
||
"prompt": "lightmagenta bold",
|
||
"code": "yellow",
|
||
"url": "lightblue underline",
|
||
},
|
||
"light": {
|
||
"user": "blue bold",
|
||
"agent": "darkgreen",
|
||
"system": "darkyellow",
|
||
"error": "darkred bold",
|
||
"info": "darkblue",
|
||
"muted": "gray",
|
||
"prompt": "darkmagenta bold",
|
||
"code": "darkyellow",
|
||
"url": "darkblue underline",
|
||
},
|
||
"monokai": {
|
||
"user": "cyan bold",
|
||
"agent": "green",
|
||
"system": "yellow",
|
||
"error": "red bold",
|
||
"info": "blue",
|
||
"muted": "gray",
|
||
"prompt": "magenta bold",
|
||
"code": "yellow",
|
||
"url": "cyan underline",
|
||
},
|
||
}
|
||
theme_dict = themes.get(theme_name, themes["default"])
|
||
return Style.from_dict(theme_dict)
|
||
|
||
@staticmethod
|
||
def get_ansi_theme(theme_name: str) -> Dict[str, str]:
|
||
"""Get ANSI color theme for non-prompt_toolkit output"""
|
||
themes = {
|
||
"default": {
|
||
"user": f"{Colors.CYAN}{Colors.BOLD}",
|
||
"agent": Colors.GREEN,
|
||
"system": Colors.YELLOW,
|
||
"error": f"{Colors.RED}{Colors.BOLD}",
|
||
"info": Colors.BLUE,
|
||
"muted": Colors.BRIGHT_BLACK,
|
||
"prompt": f"{Colors.MAGENTA}{Colors.BOLD}",
|
||
"code": Colors.YELLOW,
|
||
"reset": Colors.RESET,
|
||
},
|
||
"dark": {
|
||
"user": Colors.BRIGHT_CYAN,
|
||
"agent": Colors.BRIGHT_GREEN,
|
||
"system": Colors.BRIGHT_YELLOW,
|
||
"error": f"{Colors.BRIGHT_RED}{Colors.BOLD}",
|
||
"info": Colors.BRIGHT_BLUE,
|
||
"muted": Colors.BRIGHT_BLACK,
|
||
"prompt": Colors.BRIGHT_MAGENTA,
|
||
"code": Colors.YELLOW,
|
||
"reset": Colors.RESET,
|
||
},
|
||
"light": {
|
||
"user": Colors.BLUE,
|
||
"agent": "\033[32m", # darkgreen
|
||
"system": "\033[33m", # darkyellow
|
||
"error": "\033[31m", # darkred
|
||
"info": "\033[34m", # darkblue
|
||
"muted": "\033[90m", # brightblack/gray
|
||
"prompt": "\033[35m", # darkmagenta
|
||
"code": "\033[33m",
|
||
"reset": Colors.RESET,
|
||
},
|
||
"monokai": {
|
||
"user": Colors.CYAN,
|
||
"agent": Colors.GREEN,
|
||
"system": Colors.YELLOW,
|
||
"error": Colors.RED,
|
||
"info": Colors.BLUE,
|
||
"muted": Colors.BRIGHT_BLACK,
|
||
"prompt": Colors.MAGENTA,
|
||
"code": Colors.YELLOW,
|
||
"reset": Colors.RESET,
|
||
},
|
||
}
|
||
return themes.get(theme_name, themes["default"])
|
||
|
||
|
||
# =============================================================================
|
||
# SYNTAX HIGHLIGHTING
|
||
# =============================================================================
|
||
|
||
class SyntaxHighlighter:
|
||
"""Simple syntax highlighter for code blocks"""
|
||
|
||
# Language aliases
|
||
LANG_ALIASES = {
|
||
"py": "python",
|
||
"js": "javascript",
|
||
"ts": "typescript",
|
||
"rb": "ruby",
|
||
"sh": "bash",
|
||
"zsh": "bash",
|
||
"ps": "powershell",
|
||
"yml": "yaml",
|
||
"md": "markdown",
|
||
}
|
||
|
||
@classmethod
|
||
def get_lexer(cls, lang: str):
|
||
"""Get pygments lexer for language"""
|
||
if not PYGMENTS_AVAILABLE:
|
||
return None
|
||
|
||
lang = cls.LANG_ALIASES.get(lang.lower(), lang.lower())
|
||
try:
|
||
return get_lexer_by_name(lang)
|
||
except:
|
||
try:
|
||
return get_lexer_for_filename(f"file.{lang}")
|
||
except:
|
||
return None
|
||
|
||
@classmethod
|
||
def highlight(cls, code: str, lang: str = "text") -> str:
|
||
"""Apply syntax highlighting to code"""
|
||
if not PYGMENTS_AVAILABLE:
|
||
return code
|
||
|
||
lexer = cls.get_lexer(lang)
|
||
if not lexer:
|
||
return code
|
||
|
||
from pygments import highlight as pygments_highlight
|
||
from pygments.formatters import Terminal256Formatter
|
||
formatter = Terminal256Formatter()
|
||
|
||
try:
|
||
highlighted = pygments_highlight(code, lexer, formatter)
|
||
return highlighted.rstrip()
|
||
except:
|
||
return code
|
||
|
||
|
||
# =============================================================================
|
||
# MAIN ENHANCED CLI CLASS
|
||
# =============================================================================
|
||
|
||
class EnhancedCLI:
|
||
"""
|
||
Enhanced Command Line Interface for LangCode Agent.
|
||
|
||
Features:
|
||
- Colored input/output with themes
|
||
- Command history (persistent)
|
||
- Autocomplete for commands and paths
|
||
- Multi-line input support
|
||
- Code syntax highlighting
|
||
- Mouse support for scrolling
|
||
- Copy/paste support
|
||
- Signal handling (Ctrl+C, Ctrl+L)
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
history_file: str = "./.langcode/.cli_history",
|
||
theme: str = "default",
|
||
welcome_message: Optional[str] = None,
|
||
show_timestamps: bool = True,
|
||
enable_mouse: bool = True,
|
||
enable_syntax_highlight: bool = True,
|
||
terminal_width: Optional[int] = None,
|
||
):
|
||
"""
|
||
Initialize Enhanced CLI.
|
||
|
||
Args:
|
||
history_file: Path to persist command history
|
||
theme: Color theme (default, dark, light, monokai)
|
||
welcome_message: Initial message to show
|
||
show_timestamps: Show timestamps in output
|
||
enable_mouse: Enable mouse for scrolling
|
||
enable_syntax_highlight: Highlight code blocks
|
||
terminal_width: Force terminal width
|
||
"""
|
||
self.history_file = history_file
|
||
self.theme = theme
|
||
self.show_timestamps = show_timestamps
|
||
self.enable_mouse = enable_mouse
|
||
self.enable_syntax_highlight = enable_syntax_highlight and PYGMENTS_AVAILABLE
|
||
|
||
# Get terminal size
|
||
self.terminal_width = terminal_width or shutil.get_terminal_size().columns
|
||
|
||
# ANSI theme for non-prompt output
|
||
self.ansi_theme = Theme.get_ansi_theme(theme)
|
||
|
||
# Welcome message
|
||
self.welcome_message = welcome_message or self._default_welcome()
|
||
|
||
# Ensure history directory exists
|
||
os.makedirs(os.path.dirname(history_file) or ".", exist_ok=True)
|
||
|
||
# Command definitions
|
||
self.commands = {
|
||
"/help": "Mostra aiuto",
|
||
"/status": "Stato sistema",
|
||
"/clear": "Pulisci schermo",
|
||
"/theme": "Cicla tra i temi",
|
||
"/history": "Mostra cronologia",
|
||
"/theme-default": "Tema default",
|
||
"/theme-dark": "Tema dark",
|
||
"/theme-light": "Tema light",
|
||
"/theme-monokai": "Tema monokai",
|
||
"/timestamps": "Toggle timestamps",
|
||
"/syntax": "Toggle syntax highlighting",
|
||
"/width": "Show terminal width",
|
||
"/exit": "Esci",
|
||
"/quit": "Esci",
|
||
}
|
||
|
||
# Path completer for commands that need paths
|
||
self.path_completer = PathCompleter()
|
||
|
||
# Create key bindings
|
||
self.key_bindings = self._create_key_bindings()
|
||
|
||
# Create prompt session
|
||
self.session = self._create_session()
|
||
|
||
# Current theme name
|
||
self.current_theme = theme
|
||
|
||
# Statistics
|
||
self.stats = {
|
||
"commands_executed": 0,
|
||
"session_start": datetime.now(),
|
||
}
|
||
|
||
# Callback for Ctrl+L (clear)
|
||
self._on_clear: Optional[Callable] = None
|
||
|
||
def _default_welcome(self) -> str:
|
||
"""Generate default welcome message"""
|
||
theme = self.ansi_theme
|
||
w = self.terminal_width
|
||
|
||
# Box drawing
|
||
top = f"{theme['prompt']}╔{'═' * (w - 4)}╗{theme['reset']}"
|
||
bottom = f"{theme['prompt']}╚{'═' * (w - 4)}╝{theme['reset']}"
|
||
|
||
lines = [
|
||
f"{theme['prompt']}║{theme['reset']} {theme['agent']}{Colors.BOLD}LangCode Agent - Enhanced CLI{theme['reset']}" + " " * (w - 43) + f"{theme['prompt']}║{theme['reset']}",
|
||
f"{theme['prompt']}║{theme['reset']}" + " " * (w - 2) + f"{theme['prompt']}║{theme['reset']}",
|
||
f"{theme['prompt']}║{theme['reset']} {theme['system']}Comandi:{theme['reset']}" + " " * (w - 17) + f"{theme['prompt']}║{theme['reset']}",
|
||
f"{theme['prompt']}║{theme['reset']} {theme['user']}/help{theme['reset']} - Mostra aiuto {theme['user']}/clear{theme['reset']} - Pulisci {theme['prompt']}║{theme['reset']}",
|
||
f"{theme['prompt']}║{theme['reset']} {theme['user']}/status{theme['reset']} - Stato sistema {theme['user']}/theme{theme['reset']} - Cambia tema {theme['prompt']}║{theme['reset']}",
|
||
f"{theme['prompt']}║{theme['reset']} {theme['user']}/exit{theme['reset']} - Esci {theme['prompt']}║{theme['reset']}",
|
||
f"{theme['prompt']}║{theme['reset']}" + " " * (w - 2) + f"{theme['prompt']}║{theme['reset']}",
|
||
f"{theme['prompt']}║{theme['reset']} {theme['info']}Funzionalità:{theme['reset']}" + " " * (w - 22) + f"{theme['prompt']}║{theme['reset']}",
|
||
f"{theme['prompt']}║{theme['reset']} • History (↑↓) • Autocomplete (Tab) • Colors {theme['prompt']}║{theme['reset']}",
|
||
f"{theme['prompt']}║{theme['reset']} • Syntax highlighting • Mouse scroll • Multiline {theme['prompt']}║{theme['reset']}",
|
||
f"{theme['prompt']}║{theme['reset']}" + " " * (w - 2) + f"{theme['prompt']}║{theme['reset']}",
|
||
]
|
||
|
||
return "\n".join([top] + lines + [bottom])
|
||
|
||
def _create_key_bindings(self) -> KeyBindings:
|
||
"""Create custom key bindings"""
|
||
kb = KeyBindings()
|
||
|
||
@kb.add("c-c")
|
||
def _(event):
|
||
"""Handle Ctrl+C - interrupt or exit"""
|
||
# If there's text, clear it; otherwise exit
|
||
if event.app.current_buffer.text:
|
||
event.app.current_buffer.text = ""
|
||
else:
|
||
event.app.exit(exception=KeyboardInterrupt)
|
||
|
||
@kb.add("c-l")
|
||
def _(event):
|
||
"""Handle Ctrl+L - clear screen"""
|
||
os.system("clear" if os.name != "nt" else "cls")
|
||
if self._on_clear:
|
||
self._on_clear()
|
||
|
||
@kb.add("c-a")
|
||
def _(event):
|
||
"""Handle Ctrl+A - go to start of line"""
|
||
buffer = event.app.current_buffer
|
||
buffer.cursor_position = 0
|
||
|
||
@kb.add("c-e")
|
||
def _(event):
|
||
"""Handle Ctrl+E - go to end of line"""
|
||
buffer = event.app.current_buffer
|
||
buffer.cursor_position = len(buffer.text)
|
||
|
||
@kb.add("c-k")
|
||
def _(event):
|
||
"""Handle Ctrl+K - kill line to end"""
|
||
buffer = event.app.current_buffer
|
||
buffer.text = buffer.text[:buffer.cursor_position]
|
||
|
||
@kb.add("c-u")
|
||
def _(event):
|
||
"""Handle Ctrl+U - kill line from start"""
|
||
buffer = event.app.current_buffer
|
||
buffer.text = buffer.text[buffer.cursor_position:]
|
||
buffer.cursor_position = 0
|
||
|
||
@kb.add("c-w")
|
||
def _(event):
|
||
"""Handle Ctrl+W - kill word before cursor"""
|
||
buffer = event.app.current_buffer
|
||
text = buffer.text[:buffer.cursor_position]
|
||
if text and text[-1] == " ":
|
||
buffer.text = text[:-1] + buffer.text[buffer.cursor_position:]
|
||
buffer.cursor_position = len(buffer.text) - (len(buffer.text) - buffer.cursor_position + 1)
|
||
else:
|
||
words = text.split()
|
||
if words:
|
||
remaining = " ".join(words[:-1]) + (" " if len(words) > 1 else "")
|
||
buffer.text = remaining + buffer.text[buffer.cursor_position:]
|
||
buffer.cursor_position = len(remaining)
|
||
|
||
# Note: Mouse support is enabled via mouse_support parameter in PromptSession
|
||
# Scroll events are handled automatically by prompt_toolkit
|
||
|
||
return kb
|
||
|
||
def _create_session(self) -> PromptSession:
|
||
"""Create prompt session with history and autocomplete"""
|
||
|
||
# Create nested completer for commands
|
||
command_completer = NestedCompleter.from_nested_dict({
|
||
cmd: None for cmd in self.commands.keys()
|
||
})
|
||
|
||
return PromptSession(
|
||
history=FileHistory(self.history_file),
|
||
completer=command_completer,
|
||
key_bindings=self.key_bindings,
|
||
auto_suggest=AutoSuggestFromHistory(),
|
||
multiline=True,
|
||
mouse_support=self.enable_mouse,
|
||
)
|
||
|
||
def _get_prompt_tokens(self) -> FormattedText:
|
||
"""Create prompt tokens with colors and optional info"""
|
||
theme = self.ansi_theme
|
||
timestamp = datetime.now().strftime("%H:%M") if self.show_timestamps else None
|
||
|
||
tokens = []
|
||
|
||
# Prompt symbol with theme
|
||
tokens.append(("class:prompt", f"{theme['prompt']}❯ {theme['reset']}"))
|
||
tokens.append(("class:user", f"{theme['user']}TUO{theme['reset']}"))
|
||
|
||
if timestamp:
|
||
tokens.append(("class:muted", f" [{timestamp}]"))
|
||
|
||
tokens.append(("class:prompt", f" {theme['prompt']}»{theme['reset']} "))
|
||
|
||
return FormattedText(tokens)
|
||
|
||
def _print_with_timestamp(self, message: str, prefix: str = "") -> None:
|
||
"""Print message with optional timestamp"""
|
||
if self.show_timestamps:
|
||
ts = datetime.now().strftime("%H:%M:%S")
|
||
print(f"{self.ansi_theme['muted']}[{ts}]{self.ansi_theme['reset']} {prefix}{message}")
|
||
else:
|
||
print(f"{prefix}{message}")
|
||
|
||
def _print_formatted(self, message: str, level: str = "info") -> None:
|
||
"""Print formatted message with color and optional timestamp"""
|
||
theme = self.ansi_theme
|
||
colors = {
|
||
"user": theme["user"],
|
||
"agent": theme["agent"],
|
||
"system": theme["system"],
|
||
"error": theme["error"],
|
||
"info": theme["info"],
|
||
}
|
||
color = colors.get(level, theme["reset"])
|
||
prefix_map = {
|
||
"user": f"{color}👤{theme['reset']} ",
|
||
"agent": f"{color}🤖{theme['reset']} ",
|
||
"system": f"{color}🔧{theme['reset']} ",
|
||
"error": f"{color}❌{theme['reset']} ",
|
||
"info": f"{color}ℹ️{theme['reset']} ",
|
||
}
|
||
prefix = prefix_map.get(level, "")
|
||
self._print_with_timestamp(message, prefix)
|
||
|
||
def _print_user(self, message: str) -> None:
|
||
"""Print user message"""
|
||
self._print_formatted(message, "user")
|
||
|
||
def _print_agent(self, message: str) -> None:
|
||
"""Print agent response with optional code highlighting"""
|
||
if self.enable_syntax_highlight:
|
||
message = self._highlight_code_blocks(message)
|
||
self._print_formatted(message, "agent")
|
||
|
||
def _print_system(self, message: str) -> None:
|
||
"""Print system message"""
|
||
self._print_formatted(message, "system")
|
||
|
||
def _print_error(self, message: str) -> None:
|
||
"""Print error message"""
|
||
self._print_formatted(message, "error")
|
||
|
||
def _print_info(self, message: str) -> None:
|
||
"""Print info message"""
|
||
self._print_formatted(message, "info")
|
||
|
||
def _highlight_code_blocks(self, text: str) -> str:
|
||
"""Apply syntax highlighting to code blocks in text"""
|
||
if not self.enable_syntax_highlight:
|
||
return text
|
||
|
||
# Pattern: ```language\ncode\n```
|
||
code_block_pattern = r"```(\w+)?\n(.*?)```"
|
||
|
||
def replace_block(match):
|
||
lang = match.group(1) or "text"
|
||
code = match.group(2)
|
||
|
||
if PYGMENTS_AVAILABLE and lang.lower() not in ["text", "txt", "ansi"]:
|
||
try:
|
||
lexer = SyntaxHighlighter.get_lexer(lang)
|
||
if lexer:
|
||
from pygments import highlight as pygments_highlight
|
||
from pygments.formatters import Terminal256Formatter
|
||
highlighted = pygments_highlight(code, lexer, Terminal256Formatter())
|
||
return highlighted.rstrip()
|
||
except:
|
||
pass
|
||
|
||
# No highlighting available, just return code with a header
|
||
return f"{self.ansi_theme['code']}--- {lang or 'code'} ---\n{code}{self.ansi_theme['reset']}"
|
||
|
||
return re.sub(code_block_pattern, replace_block, text, flags=re.DOTALL)
|
||
|
||
def _handle_command(self, command: str) -> Optional[bool]:
|
||
"""Handle special commands (/command)."""
|
||
cmd_lower = command.lower().strip()
|
||
|
||
if cmd_lower == "/help":
|
||
self._print_system("""
|
||
╔══════════════════════════════════════════════════════════════════╗
|
||
║ COMANDI DISPONIBILI ║
|
||
╠══════════════════════════════════════════════════════════════════╣
|
||
║ /help - Mostra questo messaggio ║
|
||
║ /status - Mostra stato del sistema ║
|
||
║ /clear, Ctrl+L - Pulisci lo schermo ║
|
||
║ /theme - Cicla tra i temi ║
|
||
║ /theme-default - Tema default ║
|
||
║ /theme-dark - Tema scuro ║
|
||
║ /theme-light - Tema chiaro ║
|
||
║ /theme-monokai - Tema monokai ║
|
||
║ /timestamps - Toggle timestamps ║
|
||
║ /syntax - Toggle syntax highlighting ║
|
||
║ /width - Mostra larghezza terminale ║
|
||
║ /history - Mostra cronologia comandi ║
|
||
║ /exit, Ctrl+C - Esci dall'agente ║
|
||
╠══════════════════════════════════════════════════════════════════╣
|
||
║ SHORTCUTS: ║
|
||
║ Ctrl+A/E - Inizio/fine riga ║
|
||
║ Ctrl+K/U - Kill riga da cursore/fino a cursore ║
|
||
║ Ctrl+W - Kill parola precedente ║
|
||
║ ↑/↓ - History navigation ║
|
||
║ Tab - Autocomplete ║
|
||
║ Shift+Tab - Show suggestions ║
|
||
╚══════════════════════════════════════════════════════════════════╝
|
||
""")
|
||
return None
|
||
|
||
elif cmd_lower == "/status":
|
||
uptime = datetime.now() - self.stats["session_start"]
|
||
self._print_info(f"Theme: {self.current_theme}")
|
||
self._print_info(f"Timestamps: {'On' if self.show_timestamps else 'Off'}")
|
||
self._print_info(f"Syntax highlighting: {'On' if self.enable_syntax_highlight else 'Off'}")
|
||
self._print_info(f"Commands executed: {self.stats['commands_executed']}")
|
||
self._print_info(f"Session uptime: {uptime}")
|
||
self._print_info(f"Terminal width: {self.terminal_width}")
|
||
self._print_info(f"History file: {self.history_file}")
|
||
return None
|
||
|
||
elif cmd_lower == "/clear":
|
||
os.system("clear" if os.name != "nt" else "cls")
|
||
return None
|
||
|
||
elif cmd_lower in ["/theme", "/theme-default"]:
|
||
self.current_theme = "default"
|
||
self.ansi_theme = Theme.get_ansi_theme("default")
|
||
self._print_info("Tema cambiato a: default")
|
||
return None
|
||
|
||
elif cmd_lower == "/theme-dark":
|
||
self.current_theme = "dark"
|
||
self.ansi_theme = Theme.get_ansi_theme("dark")
|
||
self._print_info("Tema cambiato a: dark")
|
||
return None
|
||
|
||
elif cmd_lower == "/theme-light":
|
||
self.current_theme = "light"
|
||
self.ansi_theme = Theme.get_ansi_theme("light")
|
||
self._print_info("Tema cambiato a: light")
|
||
return None
|
||
|
||
elif cmd_lower == "/theme-monokai":
|
||
self.current_theme = "monokai"
|
||
self.ansi_theme = Theme.get_ansi_theme("monokai")
|
||
self._print_info("Tema cambiato a: monokai")
|
||
return None
|
||
|
||
elif cmd_lower == "/timestamps":
|
||
self.show_timestamps = not self.show_timestamps
|
||
self._print_info(f"Timestamps: {'On' if self.show_timestamps else 'Off'}")
|
||
return None
|
||
|
||
elif cmd_lower == "/syntax":
|
||
self.enable_syntax_highlight = not self.enable_syntax_highlight
|
||
self._print_info(f"Syntax highlighting: {'On' if self.enable_syntax_highlight else 'Off'}")
|
||
return None
|
||
|
||
elif cmd_lower == "/width":
|
||
self.terminal_width = shutil.get_terminal_size().columns
|
||
self._print_info(f"Terminal width: {self.terminal_width}")
|
||
return None
|
||
|
||
elif cmd_lower in ["/exit", "/quit"]:
|
||
return True
|
||
|
||
elif cmd_lower == "/history":
|
||
try:
|
||
with open(self.history_file, "r") as f:
|
||
lines = f.readlines()
|
||
if lines:
|
||
self._print_system(f"Ultimi {min(20, len(lines))} comandi:")
|
||
for i, line in enumerate(lines[-20:], 1):
|
||
print(f" {i:3d}. {line.strip()}")
|
||
else:
|
||
self._print_info("Nessuna cronologia")
|
||
except FileNotFoundError:
|
||
self._print_info("Nessuna cronologia")
|
||
return None
|
||
|
||
return None # Command not recognized
|
||
|
||
def prompt(self) -> str:
|
||
"""Get input from user with enhanced UI"""
|
||
try:
|
||
text = self.session.prompt(
|
||
self._get_prompt_tokens(),
|
||
style=Theme.get_style(self.current_theme),
|
||
)
|
||
return text
|
||
except KeyboardInterrupt:
|
||
return ""
|
||
except EOFError:
|
||
return "/exit"
|
||
|
||
def run(self, agent_callback: Callable[[str], str]):
|
||
"""
|
||
Run the interactive CLI loop.
|
||
|
||
Args:
|
||
agent_callback: Function that takes user input and returns agent response
|
||
"""
|
||
import signal
|
||
import sys
|
||
|
||
# Register signal handler for Ctrl+C
|
||
def handle_sigint(signum, frame):
|
||
print("\n\n👋 Arrivederci!")
|
||
sys.exit(0)
|
||
|
||
signal.signal(signal.SIGINT, handle_sigint)
|
||
|
||
# Print welcome
|
||
print(self.welcome_message)
|
||
|
||
while True:
|
||
try:
|
||
# Get user input
|
||
user_input = self.prompt().strip()
|
||
|
||
if not user_input:
|
||
continue
|
||
|
||
# Show what user typed
|
||
self._print_user(user_input)
|
||
self.stats["commands_executed"] += 1
|
||
|
||
# Check for commands
|
||
if user_input.startswith("/"):
|
||
should_exit = self._handle_command(user_input)
|
||
if should_exit:
|
||
break
|
||
continue
|
||
|
||
# Call agent and show response
|
||
try:
|
||
response = agent_callback(user_input)
|
||
self._print_agent(response)
|
||
except KeyboardInterrupt:
|
||
self._print_system("\nInterruzione... Riprova.")
|
||
except Exception as e:
|
||
self._print_error(f"Errore: {str(e)}")
|
||
|
||
except KeyboardInterrupt:
|
||
self._print_system("\n👋 Arrivederci!")
|
||
break
|
||
except EOFError:
|
||
break
|
||
|
||
self._print_system("Sessione terminata.")
|
||
|
||
|
||
# =============================================================================
|
||
# FACTORY FUNCTION
|
||
# =============================================================================
|
||
|
||
def create_enhanced_cli(
|
||
history_file: str = "./.langcode/.cli_history",
|
||
theme: str = "default",
|
||
**kwargs
|
||
) -> EnhancedCLI:
|
||
"""Create an EnhancedCLI instance"""
|
||
return EnhancedCLI(
|
||
history_file=history_file,
|
||
theme=theme,
|
||
**kwargs
|
||
)
|
||
|
||
|
||
# =============================================================================
|
||
# STANDALONE TEST
|
||
# =============================================================================
|
||
|
||
if __name__ == "__main__":
|
||
def test_callback(text):
|
||
return f"Hai detto: {text}\n\n```python\nprint('Hello!')\n```"
|
||
|
||
cli = create_enhanced_cli(theme="monokai")
|
||
cli.run(test_callback)
|