feat: potenzia EnhancedCLI con funzionalità avanzate
Features aggiunte: - 4 temi: default, dark, light, monokai - Syntax highlighting per code blocks (pygments) - Keyboard shortcuts: Ctrl+A/E (inizio/fine), Ctrl+K/U (kill line), Ctrl+W (kill word) - Toggle timestamps, syntax con comandi dedicati - Status avanzato con uptime e statistics - Codici ANSI predefiniti per ogni tema - Mouse support per scrolling - Pygments per syntax highlighting di codice
This commit is contained in:
1 parent
4a0342dcb7
commit
9fa564e12b
1 file changed
+496
-152
+496
-152
@@ -1,32 +1,49 @@
|
||||
"""
|
||||
Enhanced CLI for LangCode Agent
|
||||
Provides modern terminal UI with colors, history, and autocomplete
|
||||
Provides modern terminal UI with colors, history, autocomplete, and more.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from typing import Optional, List, Dict, Any
|
||||
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
|
||||
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
|
||||
# =============================================================================
|
||||
|
||||
# Color palette
|
||||
class Colors:
|
||||
"""ANSI color codes for terminal output"""
|
||||
# Styles
|
||||
RESET = "\033[0m"
|
||||
BOLD = "\033[1m"
|
||||
DIM = "\033[2m"
|
||||
ITALIC = "\033[3m"
|
||||
RESET = "\033[0m"
|
||||
|
||||
# Colors
|
||||
BLACK = "\033[30m"
|
||||
RED = "\033[31m"
|
||||
GREEN = "\033[32m"
|
||||
@@ -36,12 +53,6 @@ class Colors:
|
||||
CYAN = "\033[36m"
|
||||
WHITE = "\033[37m"
|
||||
|
||||
# Background
|
||||
BG_BLACK = "\033[40m"
|
||||
BG_BLUE = "\033[44m"
|
||||
BG_CYAN = "\033[46m"
|
||||
|
||||
# Bright colors
|
||||
BRIGHT_BLACK = "\033[90m"
|
||||
BRIGHT_RED = "\033[91m"
|
||||
BRIGHT_GREEN = "\033[92m"
|
||||
@@ -55,82 +66,220 @@ class Colors:
|
||||
class Theme:
|
||||
"""Color themes for the CLI"""
|
||||
|
||||
DEFAULT = {
|
||||
"user": "ansicyan", # Cyan for user input
|
||||
"agent": "ansigreen", # Green for agent response
|
||||
"system": "ansiyellow", # Yellow for system messages
|
||||
"error": "ansired", # Red for errors
|
||||
"info": "ansiblue", # Blue for info
|
||||
"muted": "ansiblack", # Black for muted text
|
||||
"prompt": "ansimagenta", # Magenta for prompt
|
||||
}
|
||||
@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)
|
||||
|
||||
DARK = {
|
||||
"user": "cyan",
|
||||
"agent": "lightgreen",
|
||||
"system": "yellow",
|
||||
"error": "lightred",
|
||||
"info": "lightblue",
|
||||
"muted": "gray",
|
||||
"prompt": "magenta",
|
||||
}
|
||||
|
||||
LIGHT = {
|
||||
"user": "blue",
|
||||
"agent": "darkgreen",
|
||||
"system": "darkyellow",
|
||||
"error": "darkred",
|
||||
"info": "darkblue",
|
||||
"muted": "gray",
|
||||
"prompt": "darkmagenta",
|
||||
}
|
||||
@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"])
|
||||
|
||||
|
||||
def create_style(theme_name: str = "default") -> Style:
|
||||
"""Create prompt_toolkit style from theme"""
|
||||
theme = getattr(Theme, theme_name.upper(), Theme.DEFAULT)
|
||||
# =============================================================================
|
||||
# SYNTAX HIGHLIGHTING
|
||||
# =============================================================================
|
||||
|
||||
class SyntaxHighlighter:
|
||||
"""Simple syntax highlighter for code blocks"""
|
||||
|
||||
style_dict = {
|
||||
"user": theme["user"],
|
||||
"agent": theme["agent"],
|
||||
"system": theme["system"],
|
||||
"error": theme["error"],
|
||||
"info": theme["info"],
|
||||
"muted": theme["muted"],
|
||||
"prompt": theme["prompt"],
|
||||
# Language aliases
|
||||
LANG_ALIASES = {
|
||||
"py": "python",
|
||||
"js": "javascript",
|
||||
"ts": "typescript",
|
||||
"rb": "ruby",
|
||||
"sh": "bash",
|
||||
"zsh": "bash",
|
||||
"ps": "powershell",
|
||||
"yml": "yaml",
|
||||
"md": "markdown",
|
||||
}
|
||||
|
||||
return Style.from_dict(style_dict)
|
||||
@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
|
||||
- Colored input/output with themes
|
||||
- Command history (persistent)
|
||||
- Autocomplete for commands
|
||||
- Autocomplete for commands and paths
|
||||
- Multi-line input support
|
||||
- Themes (light/dark)
|
||||
- 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
|
||||
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)
|
||||
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
|
||||
@@ -141,16 +290,21 @@ class EnhancedCLI:
|
||||
"/help": "Mostra aiuto",
|
||||
"/status": "Stato sistema",
|
||||
"/clear": "Pulisci schermo",
|
||||
"/theme": "Cambia tema (default/dark/light)",
|
||||
"/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",
|
||||
}
|
||||
|
||||
# Create completer for autocomplete
|
||||
self.completer = NestedCompleter.from_nested_dict({
|
||||
cmd: None for cmd in self.commands.keys()
|
||||
})
|
||||
# Path completer for commands that need paths
|
||||
self.path_completer = PathCompleter()
|
||||
|
||||
# Create key bindings
|
||||
self.key_bindings = self._create_key_bindings()
|
||||
@@ -161,23 +315,39 @@ class EnhancedCLI:
|
||||
# Current theme name
|
||||
self.current_theme = theme
|
||||
|
||||
# Output welcome message
|
||||
msg = welcome_message if welcome_message else self._default_welcome()
|
||||
self._print_system(msg)
|
||||
# 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"""
|
||||
return f"""
|
||||
{Colors.CYAN}{Colors.BOLD}╔═══════════════════════════════════════════════════════════╗{Colors.RESET}
|
||||
║ {Colors.BRIGHT_GREEN}{Colors.BOLD}LangCode Agent - Enhanced CLI{Colors.RESET} ║
|
||||
║ ║
|
||||
║ {Colors.YELLOW}Comandi:{Colors.RESET} ║
|
||||
║ {Colors.CYAN}/help{Colors.RESET} - Mostra aiuto {Colors.CYAN}/clear{Colors.RESET} - Pulisci schermo ║
|
||||
║ {Colors.CYAN}/status{Colors.RESET} - Stato sistema {Colors.CYAN}/theme{Colors.RESET} - Cambia tema ║
|
||||
║ {Colors.CYAN}/exit{Colors.RESET} - Esci ║
|
||||
║ ║
|
||||
╚═══════════════════════════════════════════════════════════════════╝{Colors.RESET}
|
||||
"""
|
||||
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"""
|
||||
@@ -185,94 +355,222 @@ class EnhancedCLI:
|
||||
|
||||
@kb.add("c-c")
|
||||
def _(event):
|
||||
"""Handle Ctrl+C - cancel input"""
|
||||
event.app.exit(exception=KeyboardInterrupt)
|
||||
"""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=self.completer,
|
||||
completer=command_completer,
|
||||
key_bindings=self.key_bindings,
|
||||
auto_suggest=AutoSuggestFromHistory(),
|
||||
multiline=True,
|
||||
cursor=CursorShape.BEAM,
|
||||
mouse_support=self.enable_mouse,
|
||||
)
|
||||
|
||||
def _get_bottom_bar(self, cli) -> List:
|
||||
"""Bottom toolbar showing theme and shortcuts"""
|
||||
return [
|
||||
("class:muted", f" Theme: {self.current_theme} "),
|
||||
("class:muted", " | "),
|
||||
("class:muted", "Ctrl+C: exit | Tab: autocomplete | ↑↓: history "),
|
||||
]
|
||||
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 _create_prompt_tokens(self) -> FormattedText:
|
||||
"""Create prompt tokens with colors"""
|
||||
timestamp = datetime.now().strftime("%H:%M")
|
||||
return FormattedText([
|
||||
("class:prompt", f"❯ "),
|
||||
("class:user", f"TUO "),
|
||||
("class:muted", f"[{timestamp}] "),
|
||||
])
|
||||
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_system(self, message: str) -> None:
|
||||
"""Print system message (in yellow)"""
|
||||
print(f"{Colors.YELLOW}{message}{Colors.RESET}")
|
||||
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 input (in cyan)"""
|
||||
print(f"{Colors.CYAN}👤 TUO:{Colors.RESET} {message}")
|
||||
"""Print user message"""
|
||||
self._print_formatted(message, "user")
|
||||
|
||||
def _print_agent(self, message: str) -> None:
|
||||
"""Print agent response (in green)"""
|
||||
# Handle multi-line messages
|
||||
lines = message.split("\n")
|
||||
for i, line in enumerate(lines):
|
||||
if i == 0:
|
||||
print(f"{Colors.GREEN}🤖 AGENTE:{Colors.RESET} {line}")
|
||||
else:
|
||||
print(f"{Colors.GREEN} {Colors.RESET} {line}")
|
||||
"""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 (in red)"""
|
||||
print(f"{Colors.RED}❌ ERRORE:{Colors.RESET} {message}")
|
||||
"""Print error message"""
|
||||
self._print_formatted(message, "error")
|
||||
|
||||
def _print_info(self, message: str) -> None:
|
||||
"""Print info message (in blue)"""
|
||||
print(f"{Colors.BLUE}ℹ️ INFO:{Colors.RESET} {message}")
|
||||
"""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).
|
||||
|
||||
Returns:
|
||||
True if command was handled and we should exit
|
||||
False if command was handled but we should continue
|
||||
None if command was not recognized
|
||||
"""
|
||||
"""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 - Pulisci lo schermo ║
|
||||
║ /theme - Cicla tra i temi (default/dark/light) ║
|
||||
║ /history - Mostra cronologia comandi ║
|
||||
║ /exit - Esci dall'agente ║
|
||||
║ ║
|
||||
║ Per messaggi normali, scrivi direttamente la tua domanda. ║
|
||||
╚═══════════════════════════════════════════════════════════════╝
|
||||
╔══════════════════════════════════════════════════════════════════╗
|
||||
║ 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
|
||||
|
||||
@@ -280,12 +578,43 @@ class EnhancedCLI:
|
||||
os.system("clear" if os.name != "nt" else "cls")
|
||||
return None
|
||||
|
||||
elif cmd_lower == "/theme":
|
||||
themes = ["default", "dark", "light"]
|
||||
current_idx = themes.index(self.current_theme) if self.current_theme in themes else 0
|
||||
next_theme = themes[(current_idx + 1) % len(themes)]
|
||||
self.current_theme = next_theme
|
||||
self._print_info(f"Tema cambiato a: {next_theme}")
|
||||
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"]:
|
||||
@@ -296,9 +625,9 @@ class EnhancedCLI:
|
||||
with open(self.history_file, "r") as f:
|
||||
lines = f.readlines()
|
||||
if lines:
|
||||
self._print_system(f"Ultimi {min(10, len(lines))} comandi:")
|
||||
for i, line in enumerate(lines[-10:], 1):
|
||||
print(f" {i}. {line.strip()}")
|
||||
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:
|
||||
@@ -311,23 +640,24 @@ class EnhancedCLI:
|
||||
"""Get input from user with enhanced UI"""
|
||||
try:
|
||||
text = self.session.prompt(
|
||||
self._create_prompt_tokens(),
|
||||
style=create_style(self.current_theme)
|
||||
self._get_prompt_tokens(),
|
||||
style=Theme.get_style(self.current_theme),
|
||||
)
|
||||
return text
|
||||
except KeyboardInterrupt:
|
||||
return "/exit"
|
||||
return ""
|
||||
except EOFError:
|
||||
return "/exit"
|
||||
|
||||
def run(self, agent_callback):
|
||||
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
|
||||
"""
|
||||
self._print_system(self.welcome_message)
|
||||
# Print welcome
|
||||
print(self.welcome_message)
|
||||
|
||||
while True:
|
||||
try:
|
||||
@@ -339,6 +669,7 @@ class EnhancedCLI:
|
||||
|
||||
# Show what user typed
|
||||
self._print_user(user_input)
|
||||
self.stats["commands_executed"] += 1
|
||||
|
||||
# Check for commands
|
||||
if user_input.startswith("/"):
|
||||
@@ -351,6 +682,8 @@ class EnhancedCLI:
|
||||
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)}")
|
||||
|
||||
@@ -363,19 +696,30 @@ class EnhancedCLI:
|
||||
self._print_system("Sessione terminata.")
|
||||
|
||||
|
||||
# Factory function for easy use
|
||||
# =============================================================================
|
||||
# FACTORY FUNCTION
|
||||
# =============================================================================
|
||||
|
||||
def create_enhanced_cli(
|
||||
history_file: str = "./.langcode/.cli_history",
|
||||
theme: str = "default"
|
||||
theme: str = "default",
|
||||
**kwargs
|
||||
) -> EnhancedCLI:
|
||||
"""Create an EnhancedCLI instance"""
|
||||
return EnhancedCLI(history_file=history_file, theme=theme)
|
||||
return EnhancedCLI(
|
||||
history_file=history_file,
|
||||
theme=theme,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# STANDALONE TEST
|
||||
# =============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test the CLI
|
||||
def test_callback(text):
|
||||
return f"Hai detto: {text}"
|
||||
return f"Hai detto: {text}\n\n```python\nprint('Hello!')\n```"
|
||||
|
||||
cli = create_enhanced_cli()
|
||||
cli = create_enhanced_cli(theme="monokai")
|
||||
cli.run(test_callback)
|
||||
Reference in new issue
Block a user