From 9fa564e12b6132a3adfd6a118643d4050606cedc Mon Sep 17 00:00:00 2001 From: opencode Date: Tue, 24 Mar 2026 10:23:35 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20potenzia=20EnhancedCLI=20con=20funziona?= =?UTF-8?q?lit=C3=A0=20avanzate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- enhanced_cli.py | 648 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 496 insertions(+), 152 deletions(-) diff --git a/enhanced_cli.py b/enhanced_cli.py index 2764e71..9d9bae2 100644 --- a/enhanced_cli.py +++ b/enhanced_cli.py @@ -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)