| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325 |
- """Main display coordinator for the CLI."""
- from __future__ import annotations
- import logging
- from pathlib import Path
- from typing import TYPE_CHECKING
- from rich.console import Console
- from rich.tree import Tree
- from .display_settings import DisplaySettings
- from .icon_manager import IconManager
- from .variable_display import VariableDisplayManager
- from .template_display import TemplateDisplayManager
- from .status_display import StatusDisplayManager
- from .table_display import TableDisplayManager
- if TYPE_CHECKING:
- from ..exceptions import TemplateRenderError
- from ..template import Template
- logger = logging.getLogger(__name__)
- console = Console()
- class DisplayManager:
- """Main display coordinator with shared resources.
- This class acts as a facade that delegates to specialized display managers.
- External code should use DisplayManager methods which provide backward
- compatibility while internally using the specialized managers.
- Design Principles:
- - All display logic should go through DisplayManager methods
- - IconManager is ONLY used internally by display managers
- - External code should never directly call IconManager or console.print
- - Consistent formatting across all display types
- """
- def __init__(self, quiet: bool = False, settings: DisplaySettings | None = None):
- """Initialize DisplayManager with specialized sub-managers.
- Args:
- quiet: If True, suppress all non-error output
- settings: Optional DisplaySettings instance for customization
- """
- self.quiet = quiet
- self.settings = settings or DisplaySettings()
- # Initialize specialized managers
- self.variables = VariableDisplayManager(self)
- self.templates = TemplateDisplayManager(self)
- self.status = StatusDisplayManager(self)
- self.tables = TableDisplayManager(self)
- # ===== Shared Helper Methods =====
- def _format_library_display(self, library_name: str, library_type: str) -> str:
- """Format library name with appropriate icon and color.
- Args:
- library_name: Name of the library
- library_type: Type of library ('static' or 'git')
- Returns:
- Formatted library display string with Rich markup
- """
- if library_type == "static":
- color = self.settings.COLOR_LIBRARY_STATIC
- icon = IconManager.UI_LIBRARY_STATIC
- else:
- color = self.settings.COLOR_LIBRARY_GIT
- icon = IconManager.UI_LIBRARY_GIT
- return f"[{color}]{icon} {library_name}[/{color}]"
- def _truncate_value(self, value: str, max_length: int | None = None) -> str:
- """Truncate a string value if it exceeds maximum length.
- Args:
- value: String value to truncate
- max_length: Maximum length (uses default if None)
- Returns:
- Truncated string with suffix if needed
- """
- if max_length is None:
- max_length = self.settings.VALUE_MAX_LENGTH_DEFAULT
- if max_length > 0 and len(value) > max_length:
- return (
- value[: max_length - len(self.settings.TRUNCATION_SUFFIX)]
- + self.settings.TRUNCATION_SUFFIX
- )
- return value
- def _format_file_size(self, size_bytes: int) -> str:
- """Format file size in human-readable format (B, KB, MB).
- Args:
- size_bytes: Size in bytes
- Returns:
- Formatted size string (e.g., "1.5KB", "2.3MB")
- """
- if size_bytes < self.settings.SIZE_KB_THRESHOLD:
- return f"{size_bytes}B"
- elif size_bytes < self.settings.SIZE_MB_THRESHOLD:
- kb = size_bytes / self.settings.SIZE_KB_THRESHOLD
- return f"{kb:.{self.settings.SIZE_DECIMAL_PLACES}f}KB"
- else:
- mb = size_bytes / self.settings.SIZE_MB_THRESHOLD
- return f"{mb:.{self.settings.SIZE_DECIMAL_PLACES}f}MB"
- # ===== Backward Compatibility Delegation Methods =====
- # These methods delegate to specialized managers for backward compatibility
- def display_templates_table(
- self, templates: list, module_name: str, title: str
- ) -> None:
- """Delegate to TableDisplayManager."""
- return self.tables.render_templates_table(templates, module_name, title)
- def display_template(self, template: "Template", template_id: str) -> None:
- """Delegate to TemplateDisplayManager."""
- return self.templates.render_template(template, template_id)
- def display_section(self, title: str, description: str | None) -> None:
- """Delegate to VariableDisplayManager."""
- return self.variables.render_section(title, description)
- def display_validation_error(self, message: str) -> None:
- """Delegate to StatusDisplayManager."""
- return self.status.display_validation_error(message)
- def display_message(
- self, level: str, message: str, context: str | None = None
- ) -> None:
- """Delegate to StatusDisplayManager."""
- return self.status.display_message(level, message, context)
- def display_error(self, message: str, context: str | None = None) -> None:
- """Delegate to StatusDisplayManager."""
- return self.status.display_error(message, context)
- def display_warning(self, message: str, context: str | None = None) -> None:
- """Delegate to StatusDisplayManager."""
- return self.status.display_warning(message, context)
- def display_success(self, message: str, context: str | None = None) -> None:
- """Delegate to StatusDisplayManager."""
- return self.status.display_success(message, context)
- def display_info(self, message: str, context: str | None = None) -> None:
- """Delegate to StatusDisplayManager."""
- return self.status.display_info(message, context)
- def display_version_incompatibility(
- self, template_id: str, required_version: str, current_version: str
- ) -> None:
- """Delegate to StatusDisplayManager."""
- return self.status.display_version_incompatibility(
- template_id, required_version, current_version
- )
- def display_file_generation_confirmation(
- self,
- output_dir: Path,
- files: dict[str, str],
- existing_files: list[Path] | None = None,
- ) -> None:
- """Delegate to TemplateDisplayManager."""
- return self.templates.render_file_generation_confirmation(
- output_dir, files, existing_files
- )
- def display_config_tree(
- self, spec: dict, module_name: str, show_all: bool = False
- ) -> None:
- """Delegate to TableDisplayManager."""
- return self.tables.render_config_tree(spec, module_name, show_all)
- def display_status_table(
- self,
- title: str,
- rows: list[tuple[str, str, bool]],
- columns: tuple[str, str] = ("Item", "Status"),
- ) -> None:
- """Delegate to TableDisplayManager."""
- return self.tables.render_status_table(title, rows, columns)
- def display_summary_table(self, title: str, items: dict[str, str]) -> None:
- """Delegate to TableDisplayManager."""
- return self.tables.render_summary_table(title, items)
- def display_file_operation_table(self, files: list[tuple[str, int, str]]) -> None:
- """Delegate to TableDisplayManager."""
- return self.tables.render_file_operation_table(files)
- def display_warning_with_confirmation(
- self, message: str, details: list[str] | None = None, default: bool = False
- ) -> bool:
- """Delegate to StatusDisplayManager."""
- return self.status.display_warning_with_confirmation(message, details, default)
- def display_skipped(self, message: str, reason: str | None = None) -> None:
- """Delegate to StatusDisplayManager."""
- return self.status.display_skipped(message, reason)
- def display_template_render_error(
- self, error: "TemplateRenderError", context: str | None = None
- ) -> None:
- """Delegate to StatusDisplayManager."""
- return self.status.display_template_render_error(error, context)
- # ===== Internal Helper Methods =====
- def _render_file_tree_internal(
- self, root_label: str, files: list, get_file_info: callable
- ) -> Tree:
- """Render 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
- # ===== Additional Methods =====
- 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 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_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)
|