from __future__ import annotations import logging from pathlib import Path from typing import TYPE_CHECKING from rich.console import Console from rich.table import Table from rich.tree import Tree if TYPE_CHECKING: from .exceptions import TemplateRenderError from .template import Template logger = logging.getLogger(__name__) console = Console() console_err = Console(stderr=True) class IconManager: """Centralized icon management system for consistent CLI display. This class provides standardized icons for file types, status indicators, and UI elements. Icons use Nerd Font glyphs for consistent display. Categories: - File types: .yaml, .j2, .json, .md, etc. - Status: success, warning, error, info, skipped - UI elements: folders, config, locks, etc. """ # File Type Icons FILE_FOLDER = "\uf07b" # FILE_DEFAULT = "\uf15b" # FILE_YAML = "\uf15c" # FILE_JSON = "\ue60b" # FILE_MARKDOWN = "\uf48a" # FILE_JINJA2 = "\ue235" # FILE_DOCKER = "\uf308" # FILE_COMPOSE = "\uf308" # FILE_SHELL = "\uf489" # FILE_PYTHON = "\ue73c" # FILE_TEXT = "\uf15c" # # Status Indicators STATUS_SUCCESS = "\uf00c" # (check) STATUS_ERROR = "\uf00d" # (times/x) STATUS_WARNING = "\uf071" # (exclamation-triangle) STATUS_INFO = "\uf05a" # (info-circle) STATUS_SKIPPED = "\uf05e" # (ban/circle-slash) # UI Elements UI_CONFIG = "\ue5fc" # UI_LOCK = "\uf084" # UI_SETTINGS = "\uf013" # UI_ARROW_RIGHT = "\uf061" # (arrow-right) UI_BULLET = "\uf111" # (circle) UI_LIBRARY_GIT = "\uf418" # (git icon) UI_LIBRARY_STATIC = "\uf07c" # (folder icon) @classmethod def get_file_icon(cls, file_path: str | Path) -> str: """Get the appropriate icon for a file based on its extension or name. Args: file_path: Path to the file (can be string or Path object) Returns: Unicode icon character for the file type Examples: >>> IconManager.get_file_icon("config.yaml") '\uf15c' >>> IconManager.get_file_icon("template.j2") '\ue235' """ if isinstance(file_path, str): file_path = Path(file_path) file_name = file_path.name.lower() suffix = file_path.suffix.lower() # Check for Docker Compose files compose_names = { "docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml", } if file_name in compose_names or file_name.startswith("docker-compose"): return cls.FILE_DOCKER # Check by extension extension_map = { ".yaml": cls.FILE_YAML, ".yml": cls.FILE_YAML, ".json": cls.FILE_JSON, ".md": cls.FILE_MARKDOWN, ".j2": cls.FILE_JINJA2, ".sh": cls.FILE_SHELL, ".py": cls.FILE_PYTHON, ".txt": cls.FILE_TEXT, } return extension_map.get(suffix, cls.FILE_DEFAULT) @classmethod def get_status_icon(cls, status: str) -> str: """Get the appropriate icon for a status indicator. Args: status: Status type (success, error, warning, info, skipped) Returns: Unicode icon character for the status Examples: >>> IconManager.get_status_icon("success") '✓' >>> IconManager.get_status_icon("warning") '⚠' """ status_map = { "success": cls.STATUS_SUCCESS, "error": cls.STATUS_ERROR, "warning": cls.STATUS_WARNING, "info": cls.STATUS_INFO, "skipped": cls.STATUS_SKIPPED, } return status_map.get(status.lower(), cls.STATUS_INFO) @classmethod def folder(cls) -> str: """Get the folder icon.""" return cls.FILE_FOLDER @classmethod def config(cls) -> str: """Get the config icon.""" return cls.UI_CONFIG @classmethod def lock(cls) -> str: """Get the lock icon (for sensitive variables).""" return cls.UI_LOCK @classmethod def arrow_right(cls) -> str: """Get the right arrow icon (for showing transitions/changes).""" return cls.UI_ARROW_RIGHT class DisplayManager: """Handles all rich rendering for the CLI. This class is responsible for ALL display output in the CLI, including: - Status messages (success, error, warning, info) - Tables (templates, summaries, results) - Trees (file structures, configurations) - Confirmation dialogs and prompts - Headers and sections Design Principles: - All display logic should go through DisplayManager methods - IconManager is ONLY used internally by DisplayManager - External code should never directly call IconManager or console.print - Consistent formatting across all display types """ def __init__(self, quiet: bool = False): """Initialize DisplayManager. Args: quiet: If True, suppress all non-error output """ self.quiet = quiet def display_templates_table( self, templates: list, module_name: str, title: str ) -> None: """Display a table of templates with library type indicators. Args: templates: List of Template objects module_name: Name of the module title: Title for the table """ if not templates: logger.info(f"No templates found for module '{module_name}'") return logger.info(f"Listing {len(templates)} templates for module '{module_name}'") table = Table(title=title) table.add_column("ID", style="bold", no_wrap=True) table.add_column("Name") table.add_column("Tags") table.add_column("Version", no_wrap=True) table.add_column("Schema", no_wrap=True) table.add_column("Library", no_wrap=True) for template in templates: name = template.metadata.name or "Unnamed Template" tags_list = template.metadata.tags or [] tags = ", ".join(tags_list) if tags_list else "-" version = ( str(template.metadata.version) if template.metadata.version else "" ) schema = template.schema_version if hasattr(template, 'schema_version') else "1.0" # Show library with type indicator and color library_name = template.metadata.library or "" library_type = template.metadata.library_type or "git" if library_type == "static": # Static libraries: yellow/amber color with folder icon library_display = ( f"[yellow]{IconManager.UI_LIBRARY_STATIC} {library_name}[/yellow]" ) else: # Git libraries: blue color with git icon library_display = ( f"[blue]{IconManager.UI_LIBRARY_GIT} {library_name}[/blue]" ) # Display qualified ID if present (e.g., "alloy.default") display_id = template.id table.add_row(display_id, name, tags, version, schema, library_display) console.print(table) def display_template_details( self, template: Template, template_id: str ) -> None: """Display template information panel and variables table. Args: template: Template instance to display template_id: ID of the template """ self._display_template_header(template, template_id) self._display_file_tree(template) self._display_variables_table(template) def display_section_header(self, title: str, description: str | None) -> None: """Display a section header.""" if description: console.print( f"\n[bold cyan]{title}[/bold cyan] [dim]- {description}[/dim]" ) else: console.print(f"\n[bold cyan]{title}[/bold cyan]") console.print("─" * 40, style="dim") def display_validation_error(self, message: str) -> None: """Display a validation error message.""" self.display_message("error", message) def display_message( self, level: str, message: str, context: str | None = None ) -> None: """Display a message with consistent formatting. Args: level: Message level (error, warning, success, info) message: The message to display context: Optional context information """ # Errors and warnings always go to stderr, even in quiet mode # Success and info respect quiet mode and go to stdout if level in ("error", "warning"): output_console = console_err should_print = True else: output_console = console should_print = not self.quiet if not should_print: return icon = IconManager.get_status_icon(level) colors = { "error": "red", "warning": "yellow", "success": "green", "info": "blue", } color = colors.get(level, "white") # Format message based on context if context: text = ( f"{level.capitalize()} in {context}: {message}" if level == "error" or level == "warning" else f"{context}: {message}" ) else: text = ( f"{level.capitalize()}: {message}" if level == "error" or level == "warning" else message ) output_console.print(f"[{color}]{icon} {text}[/{color}]") # Log appropriately log_message = f"{context}: {message}" if context else message log_methods = { "error": logger.error, "warning": logger.warning, "success": logger.info, "info": logger.info, } log_methods.get(level, logger.info)(log_message) def display_error(self, message: str, context: str | None = None) -> None: """Display an error message.""" self.display_message("error", message, context) def display_warning(self, message: str, context: str | None = None) -> None: """Display a warning message.""" self.display_message("warning", message, context) def display_success(self, message: str, context: str | None = None) -> None: """Display a success message.""" self.display_message("success", message, context) def display_info(self, message: str, context: str | None = None) -> None: """Display an informational message.""" self.display_message("info", message, context) def display_version_incompatibility( self, template_id: str, required_version: str, current_version: str ) -> None: """Display a version incompatibility error with upgrade instructions. Args: template_id: ID of the incompatible template required_version: Minimum CLI version required by template current_version: Current CLI version """ console_err.print() console_err.print( f"[bold red]{IconManager.STATUS_ERROR} Version Incompatibility[/bold red]" ) console_err.print() console_err.print( f"Template '[cyan]{template_id}[/cyan]' requires CLI version [green]{required_version}[/green] or higher." ) console_err.print(f"Current CLI version: [yellow]{current_version}[/yellow]") console_err.print() console_err.print("[bold]Upgrade Instructions:[/bold]") console_err.print( f" {IconManager.UI_ARROW_RIGHT} Run: [cyan]pip install --upgrade boilerplates[/cyan]" ) console_err.print( f" {IconManager.UI_ARROW_RIGHT} Or install specific version: [cyan]pip install boilerplates=={required_version}[/cyan]" ) console_err.print() logger.error( f"Template '{template_id}' requires CLI version {required_version}, " f"current version is {current_version}" ) def _display_template_header(self, template: Template, template_id: str) -> None: """Display the header for a template with library information.""" template_name = template.metadata.name or "Unnamed Template" version = ( str(template.metadata.version) if template.metadata.version else "Not specified" ) schema = template.schema_version if hasattr(template, 'schema_version') else "1.0" description = template.metadata.description or "No description available" # Get library information library_name = template.metadata.library or "" library_type = template.metadata.library_type or "git" # Format library display with icon and color if library_type == "static": library_display = ( f"[yellow]{IconManager.UI_LIBRARY_STATIC} {library_name}[/yellow]" ) else: library_display = ( f"[blue]{IconManager.UI_LIBRARY_GIT} {library_name}[/blue]" ) console.print( f"[bold blue]{template_name} ({template_id} - [cyan]{version}[/cyan] - [magenta]schema {schema}[/magenta]) {library_display}[/bold blue]" ) console.print(description) def _build_file_tree( self, root_label: str, files: list, get_file_info: callable ) -> Tree: """Build a file tree structure. Args: root_label: Label for root node files: List of files to display get_file_info: Function that takes a file and returns (path, display_name, color, extra_text) Returns: Tree object ready for display """ file_tree = Tree(root_label) tree_nodes = {Path("."): file_tree} for file_item in sorted(files, key=lambda f: get_file_info(f)[0]): path, display_name, color, extra_text = get_file_info(file_item) parts = path.parts current_path = Path(".") current_node = file_tree # Build directory structure for part in parts[:-1]: current_path = current_path / part if current_path not in tree_nodes: new_node = current_node.add( f"{IconManager.folder()} [white]{part}[/white]" ) tree_nodes[current_path] = new_node current_node = tree_nodes[current_path] # Add file icon = IconManager.get_file_icon(display_name) file_label = f"{icon} [{color}]{display_name}[/{color}]" if extra_text: file_label += f" {extra_text}" current_node.add(file_label) return file_tree def _display_file_tree(self, template: Template) -> None: """Display the file structure of a template.""" console.print() console.print("[bold blue]Template File Structure:[/bold blue]") def get_template_file_info(template_file): display_name = ( template_file.output_path.name if hasattr(template_file, "output_path") else template_file.relative_path.name ) return (template_file.relative_path, display_name, "white", None) file_tree = self._build_file_tree( f"{IconManager.folder()} [white]{template.id}[/white]", template.template_files, get_template_file_info, ) if file_tree.children: console.print(file_tree) def _display_variables_table( self, template: Template ) -> None: """Display a table of variables for a template. All variables and sections are always shown. Disabled sections/variables are displayed with dimmed styling. Args: template: Template instance """ if not (template.variables and template.variables.has_sections()): return console.print() console.print("[bold blue]Template Variables:[/bold blue]") variables_table = Table(show_header=True, header_style="bold blue") variables_table.add_column("Variable", style="white", no_wrap=True) variables_table.add_column("Type", style="magenta") variables_table.add_column("Default", style="green") variables_table.add_column("Description", style="white") first_section = True for section in template.variables.get_sections().values(): if not section.variables: continue if not first_section: variables_table.add_row("", "", "", "", style="bright_black") first_section = False # Check if section is enabled AND dependencies are satisfied is_enabled = section.is_enabled() dependencies_satisfied = template.variables.is_section_satisfied( section.key ) is_dimmed = not (is_enabled and dependencies_satisfied) # Only show (disabled) if section has no dependencies (dependencies make it obvious) # Empty list means no dependencies (same as None) has_dependencies = section.needs and len(section.needs) > 0 disabled_text = ( " (disabled)" if (is_dimmed and not has_dependencies) else "" ) # For disabled sections, make entire heading bold and dim (don't include colored markup inside) if is_dimmed: # Build text without internal markup, then wrap entire thing in bold bright_black (dimmed appearance) required_part = " (required)" if section.required else "" header_text = f"[bold bright_black]{section.title}{required_part}{disabled_text}[/bold bright_black]" else: # For enabled sections, include the colored markup required_text = ( " [yellow](required)[/yellow]" if section.required else "" ) header_text = ( f"[bold]{section.title}{required_text}{disabled_text}[/bold]" ) variables_table.add_row(header_text, "", "", "") for var_name, variable in section.variables.items(): # Skip toggle variable in required sections (always enabled, no need to show) if section.required and section.toggle and var_name == section.toggle: continue # Check if variable's needs are satisfied var_satisfied = template.variables.is_variable_satisfied(var_name) # Dim the variable if section is dimmed OR variable needs are not satisfied row_style = "bright_black" if (is_dimmed or not var_satisfied) else None # Build default value display # Special case: disabled bool variables show as "original → False" if (is_dimmed or not var_satisfied) and variable.type == "bool": # Show that disabled bool variables are forced to False if hasattr(variable, "_original_disabled") and variable._original_disabled is not False: orig_val = str(variable._original_disabled) default_val = f"{orig_val} {IconManager.arrow_right()} False" else: default_val = "False" # If origin is 'config' and original value differs from current, show: original → config_value # BUT only for enabled variables (don't show arrow for disabled ones) elif ( not (is_dimmed or not var_satisfied) and variable.origin == "config" and hasattr(variable, "_original_stored") and variable.original_value != variable.value ): # Format original value (use same display logic, but shorter) if variable.sensitive: orig_display = "********" elif ( variable.original_value is None or variable.original_value == "" ): orig_display = "[dim](none)[/dim]" else: orig_val_str = str(variable.original_value) orig_display = ( orig_val_str[:15] + "..." if len(orig_val_str) > 15 else orig_val_str ) # Get current (config) value display (without showing "(none)" since we have the arrow) config_display = variable.get_display_value( mask_sensitive=True, max_length=15, show_none=False ) if ( not config_display ): # If still empty after show_none=False, show actual value config_display = ( str(variable.value) if variable.value else "(empty)" ) # Highlight the arrow and config value in bold yellow to show it's a custom override default_val = f"{orig_display} [bold yellow]{IconManager.arrow_right()} {config_display}[/bold yellow]" else: # Use variable's native get_display_value() method (shows "(none)" for empty) default_val = variable.get_display_value( mask_sensitive=True, max_length=30, show_none=True ) # Add lock icon for sensitive variables sensitive_icon = f" {IconManager.lock()}" if variable.sensitive else "" # Add required indicator for required variables required_indicator = ( " [yellow](required)[/yellow]" if variable.required else "" ) var_display = f" {var_name}{sensitive_icon}{required_indicator}" variables_table.add_row( var_display, variable.type or "str", default_val, variable.description or "", style=row_style, ) console.print(variables_table) def display_file_generation_confirmation( self, output_dir: Path, files: dict[str, str], existing_files: list[Path] | None = None, ) -> None: """Display files to be generated with confirmation prompt.""" console.print() console.print("[bold]Files to be generated:[/bold]") def get_file_generation_info(file_path_str): file_path = Path(file_path_str) file_name = file_path.parts[-1] if file_path.parts else file_path.name full_path = output_dir / file_path if existing_files and full_path in existing_files: return (file_path, file_name, "yellow", "[red](will overwrite)[/red]") else: return (file_path, file_name, "green", None) file_tree = self._build_file_tree( f"{IconManager.folder()} [cyan]{output_dir.resolve()}[/cyan]", files.keys(), get_file_generation_info, ) console.print(file_tree) console.print() def display_config_tree( self, spec: dict, module_name: str, show_all: bool = False ) -> None: """Display configuration spec as a tree view. Args: spec: The configuration spec dictionary module_name: Name of the module show_all: If True, show all details including descriptions """ if not spec: console.print( f"[yellow]No configuration found for module '{module_name}'[/yellow]" ) return # Create root tree node tree = Tree( f"[bold blue]{IconManager.config()} {str.capitalize(module_name)} Configuration[/bold blue]" ) for section_name, section_data in spec.items(): if not isinstance(section_data, dict): continue # Determine if this is a section with variables # Guard against None from empty YAML sections section_vars = section_data.get("vars") or {} section_desc = section_data.get("description", "") section_required = section_data.get("required", False) section_toggle = section_data.get("toggle", None) section_needs = section_data.get("needs", None) # Build section label section_label = f"[cyan]{section_name}[/cyan]" if section_required: section_label += " [yellow](required)[/yellow]" if section_toggle: section_label += f" [dim](toggle: {section_toggle})[/dim]" if section_needs: needs_str = ( ", ".join(section_needs) if isinstance(section_needs, list) else section_needs ) section_label += f" [dim](needs: {needs_str})[/dim]" if show_all and section_desc: section_label += f"\n [dim]{section_desc}[/dim]" section_node = tree.add(section_label) # Add variables if section_vars: for var_name, var_data in section_vars.items(): if isinstance(var_data, dict): var_type = var_data.get("type", "string") var_default = var_data.get("default", "") var_desc = var_data.get("description", "") var_sensitive = var_data.get("sensitive", False) # Build variable label var_label = f"[green]{var_name}[/green] [dim]({var_type})[/dim]" if var_default is not None and var_default != "": display_val = ( "********" if var_sensitive else str(var_default) ) if not var_sensitive and len(display_val) > 30: display_val = display_val[:27] + "..." var_label += f" = [yellow]{display_val}[/yellow]" if show_all and var_desc: var_label += f"\n [dim]{var_desc}[/dim]" section_node.add(var_label) else: # Simple key-value pair section_node.add( f"[green]{var_name}[/green] = [yellow]{var_data}[/yellow]" ) console.print(tree) def display_next_steps(self, next_steps: str, variable_values: dict) -> None: """Display next steps after template generation, rendering them as a Jinja2 template. Args: next_steps: The next_steps string from template metadata (may contain Jinja2 syntax) variable_values: Dictionary of variable values to use for rendering """ if not next_steps: return console.print("\n[bold cyan]Next Steps:[/bold cyan]") try: from jinja2 import Template as Jinja2Template next_steps_template = Jinja2Template(next_steps) rendered_next_steps = next_steps_template.render(variable_values) console.print(rendered_next_steps) except Exception as e: logger.warning(f"Failed to render next_steps as template: {e}") # Fallback to plain text if rendering fails console.print(next_steps) def display_status_table( self, title: str, rows: list[tuple[str, str, bool]], columns: tuple[str, str] = ("Item", "Status"), ) -> None: """Display a status table with success/error indicators. Args: title: Table title rows: List of tuples (name, message, success_bool) columns: Column headers (name_header, status_header) """ table = Table(title=title, show_header=True) table.add_column(columns[0], style="cyan", no_wrap=True) table.add_column(columns[1]) for name, message, success in rows: status_style = "green" if success else "red" status_icon = IconManager.get_status_icon("success" if success else "error") table.add_row( name, f"[{status_style}]{status_icon} {message}[/{status_style}]" ) console.print(table) def display_summary_table(self, title: str, items: dict[str, str]) -> None: """Display a simple two-column summary table. Args: title: Table title items: Dictionary of key-value pairs to display """ table = Table(title=title, show_header=False, box=None, padding=(0, 2)) table.add_column(style="bold") table.add_column() for key, value in items.items(): table.add_row(key, value) console.print(table) def display_file_operation_table(self, files: list[tuple[str, int, str]]) -> None: """Display a table of file operations with sizes and statuses. Args: files: List of tuples (file_path, size_bytes, status) """ table = Table( show_header=True, header_style="bold cyan", box=None, padding=(0, 1) ) table.add_column("File", style="white", no_wrap=False) table.add_column("Size", justify="right", style="dim") table.add_column("Status", style="yellow") for file_path, size_bytes, status in files: # Format size if size_bytes < 1024: size_str = f"{size_bytes}B" elif size_bytes < 1024 * 1024: size_str = f"{size_bytes / 1024:.1f}KB" else: size_str = f"{size_bytes / (1024 * 1024):.1f}MB" table.add_row(str(file_path), size_str, status) console.print(table) def display_heading( self, text: str, icon_type: str | None = None, style: str = "bold" ) -> None: """Display a heading with optional icon. Args: text: Heading text icon_type: Type of icon to display (e.g., 'folder', 'file', 'config') style: Rich style to apply """ if icon_type: icon = self._get_icon_by_type(icon_type) console.print(f"[{style}]{icon} {text}[/{style}]") else: console.print(f"[{style}]{text}[/{style}]") def display_warning_with_confirmation( self, message: str, details: list[str] | None = None, default: bool = False ) -> bool: """Display a warning message with optional details and get confirmation. Args: message: Warning message to display details: Optional list of detail lines to show default: Default value for confirmation Returns: True if user confirms, False otherwise """ icon = IconManager.get_status_icon("warning") console.print(f"\n[yellow]{icon} {message}[/yellow]") if details: for detail in details: console.print(f"[yellow] {detail}[/yellow]") from rich.prompt import Confirm return Confirm.ask("Continue?", default=default) def display_skipped(self, message: str, reason: str | None = None) -> None: """Display a skipped/disabled message. Args: message: The main message to display reason: Optional reason why it was skipped """ icon = IconManager.get_status_icon("skipped") if reason: console.print(f"\n[dim]{icon} {message} (skipped - {reason})[/dim]") else: console.print(f"\n[dim]{icon} {message} (skipped)[/dim]") def get_lock_icon(self) -> str: """Get the lock icon for sensitive variables. Returns: Lock icon unicode character """ return IconManager.lock() def _get_icon_by_type(self, icon_type: str) -> str: """Get icon by semantic type name. Args: icon_type: Type of icon (e.g., 'folder', 'file', 'config', 'lock') Returns: Icon unicode character """ icon_map = { "folder": IconManager.folder(), "file": IconManager.FILE_DEFAULT, "config": IconManager.config(), "lock": IconManager.lock(), "arrow": IconManager.arrow_right(), } return icon_map.get(icon_type, "") def display_template_render_error( self, error: "TemplateRenderError", context: str | None = None ) -> None: """Display a detailed template rendering error with context and suggestions. Args: error: TemplateRenderError exception with detailed error information context: Optional context information (e.g., template ID) """ from rich.panel import Panel from rich.syntax import Syntax # Always display errors to stderr # Display main error header icon = IconManager.get_status_icon("error") if context: console_err.print( f"\n[red bold]{icon} Template Rendering Error[/red bold] [dim]({context})[/dim]" ) else: console_err.print(f"\n[red bold]{icon} Template Rendering Error[/red bold]") console_err.print() # Display error message if error.file_path: console_err.print( f"[red]Error in file:[/red] [cyan]{error.file_path}[/cyan]" ) if error.line_number: location = f"Line {error.line_number}" if error.column: location += f", Column {error.column}" console_err.print(f"[red]Location:[/red] {location}") console_err.print( f"[red]Message:[/red] {str(error.original_error) if error.original_error else str(error)}" ) console_err.print() # Display code context if available if error.context_lines: console_err.print("[bold cyan]Code Context:[/bold cyan]") # Build the context text context_text = "\n".join(error.context_lines) # Display in a panel with syntax highlighting if possible file_ext = Path(error.file_path).suffix if error.file_path else "" if file_ext == ".j2": # Remove .j2 to get base extension for syntax highlighting base_name = Path(error.file_path).stem base_ext = Path(base_name).suffix lexer = "jinja2" if not base_ext else None else: lexer = None try: if lexer: syntax = Syntax( context_text, lexer, line_numbers=False, theme="monokai" ) console_err.print(Panel(syntax, border_style="red", padding=(1, 2))) else: console_err.print( Panel(context_text, border_style="red", padding=(1, 2)) ) except Exception: # Fallback to plain panel if syntax highlighting fails console_err.print( Panel(context_text, border_style="red", padding=(1, 2)) ) console_err.print() # Display suggestions if available if error.suggestions: console_err.print("[bold yellow]Suggestions:[/bold yellow]") for i, suggestion in enumerate(error.suggestions, 1): bullet = IconManager.UI_BULLET console_err.print(f" [yellow]{bullet}[/yellow] {suggestion}") console_err.print() # Display variable context in debug mode if error.variable_context: console_err.print("[bold blue]Available Variables (Debug):[/bold blue]") var_list = ", ".join(sorted(error.variable_context.keys())) console_err.print(f"[dim]{var_list}[/dim]") console_err.print()