display_table.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. from __future__ import annotations
  2. import logging
  3. from typing import TYPE_CHECKING
  4. from rich.table import Table
  5. from rich.tree import Tree
  6. from .display_icons import IconManager
  7. from .display_settings import DisplaySettings
  8. if TYPE_CHECKING:
  9. from .display_base import BaseDisplay
  10. logger = logging.getLogger(__name__)
  11. class TableDisplay:
  12. """Table rendering.
  13. Provides methods for displaying data tables with flexible formatting.
  14. """
  15. def __init__(self, settings: DisplaySettings, base: BaseDisplay):
  16. """Initialize TableDisplay.
  17. Args:
  18. settings: Display settings for formatting
  19. base: BaseDisplay instance for utility methods
  20. """
  21. self.settings = settings
  22. self.base = base
  23. def data_table(
  24. self,
  25. columns: list[dict],
  26. rows: list,
  27. title: str | None = None,
  28. row_formatter: callable | None = None,
  29. ) -> None:
  30. """Display a data table with configurable columns and formatting.
  31. Args:
  32. columns: List of column definitions, each dict with:
  33. - name: Column header text
  34. - style: Optional Rich style (e.g., "bold", "cyan")
  35. - no_wrap: Optional bool to prevent text wrapping
  36. - justify: Optional justify ("left", "right", "center")
  37. rows: List of data rows (dicts, tuples, or objects)
  38. title: Optional table title
  39. row_formatter: Optional function(row) -> tuple to transform row data
  40. """
  41. table = Table(title=title, show_header=True)
  42. # Add columns
  43. for col in columns:
  44. table.add_column(
  45. col["name"],
  46. style=col.get("style"),
  47. no_wrap=col.get("no_wrap", False),
  48. justify=col.get("justify", "left"),
  49. )
  50. # Add rows
  51. for row in rows:
  52. if row_formatter:
  53. formatted_row = row_formatter(row)
  54. elif isinstance(row, dict):
  55. formatted_row = tuple(str(row.get(col["name"], "")) for col in columns)
  56. else:
  57. formatted_row = tuple(str(cell) for cell in row)
  58. table.add_row(*formatted_row)
  59. self.base._print_table(table)
  60. def render_templates_table(
  61. self, templates: list, module_name: str, title: str
  62. ) -> None:
  63. """Display a table of templates with library type indicators.
  64. Args:
  65. templates: List of Template objects
  66. module_name: Name of the module
  67. title: Title for the table
  68. """
  69. if not templates:
  70. logger.info(f"No templates found for module '{module_name}'")
  71. return
  72. logger.info(f"Listing {len(templates)} templates for module '{module_name}'")
  73. table = Table()
  74. table.add_column("ID", style="bold", no_wrap=True)
  75. table.add_column("Name")
  76. table.add_column("Tags")
  77. table.add_column("Version", no_wrap=True)
  78. table.add_column("Schema", no_wrap=True)
  79. table.add_column("Library", no_wrap=True)
  80. settings = self.settings
  81. for template in templates:
  82. name = template.metadata.name or settings.TEXT_UNNAMED_TEMPLATE
  83. tags_list = template.metadata.tags or []
  84. tags = ", ".join(tags_list) if tags_list else "-"
  85. version = (
  86. str(template.metadata.version) if template.metadata.version else ""
  87. )
  88. schema = (
  89. template.schema_version
  90. if hasattr(template, "schema_version")
  91. else "1.0"
  92. )
  93. # Format library with icon and color
  94. library_name = template.metadata.library or ""
  95. library_type = template.metadata.library_type or "git"
  96. icon = IconManager.UI_LIBRARY_STATIC if library_type == "static" else IconManager.UI_LIBRARY_GIT
  97. color = "yellow" if library_type == "static" else "blue"
  98. library_display = f"[{color}]{icon} {library_name}[/{color}]"
  99. table.add_row(template.id, name, tags, version, schema, library_display)
  100. self.base._print_table(table)
  101. def render_status_table(
  102. self,
  103. title: str,
  104. rows: list[tuple[str, str, bool]],
  105. columns: tuple[str, str] = ("Item", "Status"),
  106. ) -> None:
  107. """Display a status table with success/error indicators.
  108. Args:
  109. title: Table title
  110. rows: List of tuples (name, message, success_bool)
  111. columns: Column headers (name_header, status_header)
  112. """
  113. table = Table(show_header=True)
  114. table.add_column(columns[0], style="cyan", no_wrap=True)
  115. table.add_column(columns[1])
  116. for name, message, success in rows:
  117. status_style = "green" if success else "red"
  118. status_icon = IconManager.get_status_icon("success" if success else "error")
  119. table.add_row(
  120. name, f"[{status_style}]{status_icon} {message}[/{status_style}]"
  121. )
  122. self.base._print_table(table)
  123. def render_summary_table(self, title: str, items: dict[str, str]) -> None:
  124. """Display a simple two-column summary table.
  125. Args:
  126. title: Table title
  127. items: Dictionary of key-value pairs to display
  128. """
  129. settings = self.settings
  130. table = Table(
  131. title=title,
  132. show_header=False,
  133. box=None,
  134. padding=settings.PADDING_TABLE_NORMAL,
  135. )
  136. table.add_column(style="bold")
  137. table.add_column()
  138. for key, value in items.items():
  139. table.add_row(key, value)
  140. self.base._print_table(table)
  141. def render_file_operation_table(self, files: list[tuple[str, int, str]]) -> None:
  142. """Display a table of file operations with sizes and statuses.
  143. Args:
  144. files: List of tuples (file_path, size_bytes, status)
  145. """
  146. settings = self.settings
  147. table = Table(
  148. show_header=True,
  149. header_style=settings.STYLE_TABLE_HEADER,
  150. box=None,
  151. padding=settings.PADDING_TABLE_COMPACT,
  152. )
  153. table.add_column("File", style="white", no_wrap=False)
  154. table.add_column("Size", justify="right", style=settings.COLOR_MUTED)
  155. table.add_column("Status", style=settings.COLOR_WARNING)
  156. for file_path, size_bytes, status in files:
  157. size_str = self.base.format_file_size(size_bytes)
  158. table.add_row(str(file_path), size_str, status)
  159. self.base._print_table(table)
  160. def _build_section_label(
  161. self,
  162. section_name: str,
  163. section_data: dict,
  164. show_all: bool,
  165. ) -> str:
  166. """Build section label with metadata."""
  167. section_desc = section_data.get("description", "")
  168. section_required = section_data.get("required", False)
  169. section_toggle = section_data.get("toggle")
  170. section_needs = section_data.get("needs")
  171. label = f"[cyan]{section_name}[/cyan]"
  172. if section_required:
  173. label += " [yellow](required)[/yellow]"
  174. if section_toggle:
  175. label += f" [dim](toggle: {section_toggle})[/dim]"
  176. if section_needs:
  177. needs_str = (
  178. ", ".join(section_needs)
  179. if isinstance(section_needs, list)
  180. else section_needs
  181. )
  182. label += f" [dim](needs: {needs_str})[/dim]"
  183. if show_all and section_desc:
  184. label += f"\n [dim]{section_desc}[/dim]"
  185. return label
  186. def _build_variable_label(
  187. self,
  188. var_name: str,
  189. var_data: dict,
  190. show_all: bool,
  191. ) -> str:
  192. """Build variable label with type and default value."""
  193. var_type = var_data.get("type", "string")
  194. var_default = var_data.get("default", "")
  195. var_desc = var_data.get("description", "")
  196. var_sensitive = var_data.get("sensitive", False)
  197. label = f"[green]{var_name}[/green] [dim]({var_type})[/dim]"
  198. if var_default is not None and var_default != "":
  199. settings = self.settings
  200. display_val = settings.SENSITIVE_MASK if var_sensitive else str(var_default)
  201. if not var_sensitive:
  202. display_val = self.base.truncate(
  203. display_val, settings.VALUE_MAX_LENGTH_DEFAULT
  204. )
  205. label += (
  206. f" = [{settings.COLOR_WARNING}]{display_val}[/{settings.COLOR_WARNING}]"
  207. )
  208. if show_all and var_desc:
  209. label += f"\n [dim]{var_desc}[/dim]"
  210. return label
  211. def _add_section_variables(
  212. self, section_node, section_vars: dict, show_all: bool
  213. ) -> None:
  214. """Add variables to a section node."""
  215. for var_name, var_data in section_vars.items():
  216. if isinstance(var_data, dict):
  217. var_label = self._build_variable_label(var_name, var_data, show_all)
  218. section_node.add(var_label)
  219. else:
  220. # Simple key-value pair
  221. section_node.add(
  222. f"[green]{var_name}[/green] = [yellow]{var_data}[/yellow]"
  223. )
  224. def render_config_tree(
  225. self, spec: dict, module_name: str, show_all: bool = False
  226. ) -> None:
  227. """Display configuration spec as a tree view.
  228. Args:
  229. spec: The configuration spec dictionary
  230. module_name: Name of the module
  231. show_all: If True, show all details including descriptions
  232. """
  233. if not spec:
  234. self.base.text(
  235. f"No configuration found for module '{module_name}'", style="yellow"
  236. )
  237. return
  238. # Create root tree node
  239. tree = Tree(
  240. f"[bold blue]{IconManager.config()} {str.capitalize(module_name)} Configuration[/bold blue]"
  241. )
  242. for section_name, section_data in spec.items():
  243. if not isinstance(section_data, dict):
  244. continue
  245. # Build and add section
  246. section_label = self._build_section_label(
  247. section_name, section_data, show_all
  248. )
  249. section_node = tree.add(section_label)
  250. # Add variables to section
  251. section_vars = section_data.get("vars") or {}
  252. if section_vars:
  253. self._add_section_variables(section_node, section_vars, show_all)
  254. self.base._print_tree(tree)