|
|
@@ -15,6 +15,130 @@ logger = logging.getLogger(__name__)
|
|
|
console = Console()
|
|
|
|
|
|
|
|
|
+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)
|
|
|
+
|
|
|
+ @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
|
|
|
+
|
|
|
+
|
|
|
class DisplayManager:
|
|
|
"""Handles all rich rendering for the CLI."""
|
|
|
|
|
|
@@ -86,7 +210,7 @@ class DisplayManager:
|
|
|
console.print()
|
|
|
console.print("[bold blue]Template File Structure:[/bold blue]")
|
|
|
# Use the template id as the root directory label (folder glyph + white name)
|
|
|
- file_tree = Tree(f"\uf07b [white]{template.id}[/white]")
|
|
|
+ file_tree = Tree(f"{IconManager.folder()} [white]{template.id}[/white]")
|
|
|
tree_nodes = {Path("."): file_tree}
|
|
|
|
|
|
for template_file in sorted(
|
|
|
@@ -99,7 +223,7 @@ class DisplayManager:
|
|
|
for part in parts[:-1]:
|
|
|
current_path = current_path / part
|
|
|
if current_path not in tree_nodes:
|
|
|
- new_node = current_node.add(f"\uf07b [white]{part}[/white]")
|
|
|
+ new_node = current_node.add(f"{IconManager.folder()} [white]{part}[/white]")
|
|
|
tree_nodes[current_path] = new_node
|
|
|
current_node = new_node
|
|
|
else:
|
|
|
@@ -108,23 +232,9 @@ class DisplayManager:
|
|
|
# Determine display name (use output_path to detect final filename)
|
|
|
display_name = template_file.output_path.name if hasattr(template_file, 'output_path') else template_file.relative_path.name
|
|
|
|
|
|
- # Default icons: use Nerd Font private-use-area codepoints (PUA).
|
|
|
- # Docker (Font Awesome) is typically U+F308. Default file U+F15B.
|
|
|
- docker_icon = "\uf308"
|
|
|
- default_file_icon = "\uf15b"
|
|
|
- j2_icon = "\ue235"
|
|
|
-
|
|
|
- if template_file.file_type == "j2":
|
|
|
- # Detect common docker compose filenames from the resulting output path
|
|
|
- lower_name = display_name.lower()
|
|
|
- compose_names = {"docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"}
|
|
|
- if lower_name in compose_names or lower_name.startswith("docker-compose") or "compose" in lower_name:
|
|
|
- icon = docker_icon
|
|
|
- else:
|
|
|
- icon = j2_icon
|
|
|
- current_node.add(f"[white]{icon} {display_name}[/white]")
|
|
|
- elif template_file.file_type == "static":
|
|
|
- current_node.add(f"[white]{default_file_icon} {display_name}[/white]")
|
|
|
+ # Get appropriate icon based on file type/name
|
|
|
+ icon = IconManager.get_file_icon(display_name)
|
|
|
+ current_node.add(f"[white]{icon} {display_name}[/white]")
|
|
|
|
|
|
if file_tree.children:
|
|
|
console.print(file_tree)
|
|
|
@@ -175,7 +285,7 @@ class DisplayManager:
|
|
|
default_val = variable.get_display_value(mask_sensitive=True, max_length=30)
|
|
|
|
|
|
# Add lock icon for sensitive variables
|
|
|
- sensitive_icon = " \uf084" if variable.sensitive else ""
|
|
|
+ sensitive_icon = f" {IconManager.lock()}" if variable.sensitive else ""
|
|
|
var_display = f" {var_name}{sensitive_icon}"
|
|
|
|
|
|
variables_table.add_row(
|
|
|
@@ -206,7 +316,7 @@ class DisplayManager:
|
|
|
console.print("[bold]Files to be generated:[/bold]")
|
|
|
|
|
|
# Create a tree view of files
|
|
|
- file_tree = Tree(f"\uf07b [cyan]{output_dir.resolve()}[/cyan]")
|
|
|
+ file_tree = Tree(f"{IconManager.folder()} [cyan]{output_dir.resolve()}[/cyan]")
|
|
|
tree_nodes = {Path("."): file_tree}
|
|
|
|
|
|
# Sort files for better display
|
|
|
@@ -222,18 +332,19 @@ class DisplayManager:
|
|
|
for part in parts[:-1]:
|
|
|
current_path = current_path / part
|
|
|
if current_path not in tree_nodes:
|
|
|
- new_node = current_node.add(f"\uf07b [white]{part}[/white]")
|
|
|
+ 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 with indicator if it will be overwritten
|
|
|
file_name = parts[-1]
|
|
|
full_path = output_dir / file_path
|
|
|
+ icon = IconManager.get_file_icon(file_name)
|
|
|
|
|
|
if existing_files and full_path in existing_files:
|
|
|
- current_node.add(f"\uf15c [yellow]{file_name}[/yellow] [red](will overwrite)[/red]")
|
|
|
+ current_node.add(f"{icon} [yellow]{file_name}[/yellow] [red](will overwrite)[/red]")
|
|
|
else:
|
|
|
- current_node.add(f"\uf15c [green]{file_name}[/green]")
|
|
|
+ current_node.add(f"{icon} [green]{file_name}[/green]")
|
|
|
|
|
|
console.print(file_tree)
|
|
|
console.print()
|
|
|
@@ -251,7 +362,7 @@ class DisplayManager:
|
|
|
return
|
|
|
|
|
|
# Create root tree node
|
|
|
- tree = Tree(f"[bold blue]\ue5fc {str.capitalize(module_name)} Configuration[/bold blue]")
|
|
|
+ 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):
|