display_manager.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. """Main display coordinator for the CLI."""
  2. from __future__ import annotations
  3. import logging
  4. from pathlib import Path
  5. from typing import TYPE_CHECKING
  6. from rich.console import Console
  7. from rich.tree import Tree
  8. from .display_settings import DisplaySettings
  9. from .icon_manager import IconManager
  10. from .variable_display import VariableDisplayManager
  11. from .template_display import TemplateDisplayManager
  12. from .status_display import StatusDisplayManager
  13. from .table_display import TableDisplayManager
  14. if TYPE_CHECKING:
  15. from ..exceptions import TemplateRenderError
  16. from ..template import Template
  17. logger = logging.getLogger(__name__)
  18. console = Console()
  19. class DisplayManager:
  20. """Main display coordinator with shared resources.
  21. This class acts as a facade that delegates to specialized display managers.
  22. External code should use DisplayManager methods which provide backward
  23. compatibility while internally using the specialized managers.
  24. Design Principles:
  25. - All display logic should go through DisplayManager methods
  26. - IconManager is ONLY used internally by display managers
  27. - External code should never directly call IconManager or console.print
  28. - Consistent formatting across all display types
  29. """
  30. def __init__(self, quiet: bool = False, settings: DisplaySettings | None = None):
  31. """Initialize DisplayManager with specialized sub-managers.
  32. Args:
  33. quiet: If True, suppress all non-error output
  34. settings: Optional DisplaySettings instance for customization
  35. """
  36. self.quiet = quiet
  37. self.settings = settings or DisplaySettings()
  38. # Initialize specialized managers
  39. self.variables = VariableDisplayManager(self)
  40. self.templates = TemplateDisplayManager(self)
  41. self.status = StatusDisplayManager(self)
  42. self.tables = TableDisplayManager(self)
  43. # ===== Shared Helper Methods =====
  44. def _format_library_display(self, library_name: str, library_type: str) -> str:
  45. """Format library name with appropriate icon and color.
  46. Args:
  47. library_name: Name of the library
  48. library_type: Type of library ('static' or 'git')
  49. Returns:
  50. Formatted library display string with Rich markup
  51. """
  52. if library_type == "static":
  53. color = self.settings.COLOR_LIBRARY_STATIC
  54. icon = IconManager.UI_LIBRARY_STATIC
  55. else:
  56. color = self.settings.COLOR_LIBRARY_GIT
  57. icon = IconManager.UI_LIBRARY_GIT
  58. return f"[{color}]{icon} {library_name}[/{color}]"
  59. def _truncate_value(self, value: str, max_length: int | None = None) -> str:
  60. """Truncate a string value if it exceeds maximum length.
  61. Args:
  62. value: String value to truncate
  63. max_length: Maximum length (uses default if None)
  64. Returns:
  65. Truncated string with suffix if needed
  66. """
  67. if max_length is None:
  68. max_length = self.settings.VALUE_MAX_LENGTH_DEFAULT
  69. if max_length > 0 and len(value) > max_length:
  70. return (
  71. value[: max_length - len(self.settings.TRUNCATION_SUFFIX)]
  72. + self.settings.TRUNCATION_SUFFIX
  73. )
  74. return value
  75. def _format_file_size(self, size_bytes: int) -> str:
  76. """Format file size in human-readable format (B, KB, MB).
  77. Args:
  78. size_bytes: Size in bytes
  79. Returns:
  80. Formatted size string (e.g., "1.5KB", "2.3MB")
  81. """
  82. if size_bytes < self.settings.SIZE_KB_THRESHOLD:
  83. return f"{size_bytes}B"
  84. elif size_bytes < self.settings.SIZE_MB_THRESHOLD:
  85. kb = size_bytes / self.settings.SIZE_KB_THRESHOLD
  86. return f"{kb:.{self.settings.SIZE_DECIMAL_PLACES}f}KB"
  87. else:
  88. mb = size_bytes / self.settings.SIZE_MB_THRESHOLD
  89. return f"{mb:.{self.settings.SIZE_DECIMAL_PLACES}f}MB"
  90. # ===== Backward Compatibility Delegation Methods =====
  91. # These methods delegate to specialized managers for backward compatibility
  92. def display_templates_table(
  93. self, templates: list, module_name: str, title: str
  94. ) -> None:
  95. """Delegate to TableDisplayManager."""
  96. return self.tables.render_templates_table(templates, module_name, title)
  97. def display_template(self, template: "Template", template_id: str) -> None:
  98. """Delegate to TemplateDisplayManager."""
  99. return self.templates.render_template(template, template_id)
  100. def display_section(self, title: str, description: str | None) -> None:
  101. """Delegate to VariableDisplayManager."""
  102. return self.variables.render_section(title, description)
  103. def display_validation_error(self, message: str) -> None:
  104. """Delegate to StatusDisplayManager."""
  105. return self.status.display_validation_error(message)
  106. def display_message(
  107. self, level: str, message: str, context: str | None = None
  108. ) -> None:
  109. """Delegate to StatusDisplayManager."""
  110. return self.status.display_message(level, message, context)
  111. def display_error(self, message: str, context: str | None = None) -> None:
  112. """Delegate to StatusDisplayManager."""
  113. return self.status.display_error(message, context)
  114. def display_warning(self, message: str, context: str | None = None) -> None:
  115. """Delegate to StatusDisplayManager."""
  116. return self.status.display_warning(message, context)
  117. def display_success(self, message: str, context: str | None = None) -> None:
  118. """Delegate to StatusDisplayManager."""
  119. return self.status.display_success(message, context)
  120. def display_info(self, message: str, context: str | None = None) -> None:
  121. """Delegate to StatusDisplayManager."""
  122. return self.status.display_info(message, context)
  123. def display_version_incompatibility(
  124. self, template_id: str, required_version: str, current_version: str
  125. ) -> None:
  126. """Delegate to StatusDisplayManager."""
  127. return self.status.display_version_incompatibility(
  128. template_id, required_version, current_version
  129. )
  130. def display_file_generation_confirmation(
  131. self,
  132. output_dir: Path,
  133. files: dict[str, str],
  134. existing_files: list[Path] | None = None,
  135. ) -> None:
  136. """Delegate to TemplateDisplayManager."""
  137. return self.templates.render_file_generation_confirmation(
  138. output_dir, files, existing_files
  139. )
  140. def display_config_tree(
  141. self, spec: dict, module_name: str, show_all: bool = False
  142. ) -> None:
  143. """Delegate to TableDisplayManager."""
  144. return self.tables.render_config_tree(spec, module_name, show_all)
  145. def display_status_table(
  146. self,
  147. title: str,
  148. rows: list[tuple[str, str, bool]],
  149. columns: tuple[str, str] = ("Item", "Status"),
  150. ) -> None:
  151. """Delegate to TableDisplayManager."""
  152. return self.tables.render_status_table(title, rows, columns)
  153. def display_summary_table(self, title: str, items: dict[str, str]) -> None:
  154. """Delegate to TableDisplayManager."""
  155. return self.tables.render_summary_table(title, items)
  156. def display_file_operation_table(self, files: list[tuple[str, int, str]]) -> None:
  157. """Delegate to TableDisplayManager."""
  158. return self.tables.render_file_operation_table(files)
  159. def display_warning_with_confirmation(
  160. self, message: str, details: list[str] | None = None, default: bool = False
  161. ) -> bool:
  162. """Delegate to StatusDisplayManager."""
  163. return self.status.display_warning_with_confirmation(message, details, default)
  164. def display_skipped(self, message: str, reason: str | None = None) -> None:
  165. """Delegate to StatusDisplayManager."""
  166. return self.status.display_skipped(message, reason)
  167. def display_template_render_error(
  168. self, error: "TemplateRenderError", context: str | None = None
  169. ) -> None:
  170. """Delegate to StatusDisplayManager."""
  171. return self.status.display_template_render_error(error, context)
  172. # ===== Internal Helper Methods =====
  173. def _render_file_tree_internal(
  174. self, root_label: str, files: list, get_file_info: callable
  175. ) -> Tree:
  176. """Render a file tree structure.
  177. Args:
  178. root_label: Label for root node
  179. files: List of files to display
  180. get_file_info: Function that takes a file and returns (path, display_name, color, extra_text)
  181. Returns:
  182. Tree object ready for display
  183. """
  184. file_tree = Tree(root_label)
  185. tree_nodes = {Path("."): file_tree}
  186. for file_item in sorted(files, key=lambda f: get_file_info(f)[0]):
  187. path, display_name, color, extra_text = get_file_info(file_item)
  188. parts = path.parts
  189. current_path = Path(".")
  190. current_node = file_tree
  191. # Build directory structure
  192. for part in parts[:-1]:
  193. current_path = current_path / part
  194. if current_path not in tree_nodes:
  195. new_node = current_node.add(
  196. f"{IconManager.folder()} [white]{part}[/white]"
  197. )
  198. tree_nodes[current_path] = new_node
  199. current_node = tree_nodes[current_path]
  200. # Add file
  201. icon = IconManager.get_file_icon(display_name)
  202. file_label = f"{icon} [{color}]{display_name}[/{color}]"
  203. if extra_text:
  204. file_label += f" {extra_text}"
  205. current_node.add(file_label)
  206. return file_tree
  207. # ===== Additional Methods =====
  208. def display_heading(
  209. self, text: str, icon_type: str | None = None, style: str = "bold"
  210. ) -> None:
  211. """Display a heading with optional icon.
  212. Args:
  213. text: Heading text
  214. icon_type: Type of icon to display (e.g., 'folder', 'file', 'config')
  215. style: Rich style to apply
  216. """
  217. if icon_type:
  218. icon = self._get_icon_by_type(icon_type)
  219. console.print(f"[{style}]{icon} {text}[/{style}]")
  220. else:
  221. console.print(f"[{style}]{text}[/{style}]")
  222. def get_lock_icon(self) -> str:
  223. """Get the lock icon for sensitive variables.
  224. Returns:
  225. Lock icon unicode character
  226. """
  227. return IconManager.lock()
  228. def _get_icon_by_type(self, icon_type: str) -> str:
  229. """Get icon by semantic type name.
  230. Args:
  231. icon_type: Type of icon (e.g., 'folder', 'file', 'config', 'lock')
  232. Returns:
  233. Icon unicode character
  234. """
  235. icon_map = {
  236. "folder": IconManager.folder(),
  237. "file": IconManager.FILE_DEFAULT,
  238. "config": IconManager.config(),
  239. "lock": IconManager.lock(),
  240. "arrow": IconManager.arrow_right(),
  241. }
  242. return icon_map.get(icon_type, "")
  243. def display_next_steps(self, next_steps: str, variable_values: dict) -> None:
  244. """Display next steps after template generation, rendering them as a Jinja2 template.
  245. Args:
  246. next_steps: The next_steps string from template metadata (may contain Jinja2 syntax)
  247. variable_values: Dictionary of variable values to use for rendering
  248. """
  249. if not next_steps:
  250. return
  251. console.print("\n[bold cyan]Next Steps:[/bold cyan]")
  252. try:
  253. from jinja2 import Template as Jinja2Template
  254. next_steps_template = Jinja2Template(next_steps)
  255. rendered_next_steps = next_steps_template.render(variable_values)
  256. console.print(rendered_next_steps)
  257. except Exception as e:
  258. logger.warning(f"Failed to render next_steps as template: {e}")
  259. # Fallback to plain text if rendering fails
  260. console.print(next_steps)