display_base.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. """Base display methods for DisplayManager."""
  2. from __future__ import annotations
  3. import logging
  4. from pathlib import Path
  5. from rich.console import Console
  6. from rich.progress import Progress
  7. from rich.syntax import Syntax
  8. from rich.table import Table
  9. from rich.tree import Tree
  10. from .display_icons import IconManager
  11. from .display_settings import DisplaySettings
  12. logger = logging.getLogger(__name__)
  13. console = Console()
  14. class BaseDisplay:
  15. """Base display methods and utilities.
  16. Provides fundamental display methods (text, heading, table, tree, code, progress)
  17. and utility/helper methods for formatting.
  18. """
  19. def __init__(self, settings: DisplaySettings, quiet: bool = False):
  20. """Initialize BaseDisplay.
  21. Args:
  22. settings: Display settings for formatting
  23. quiet: If True, suppress non-error output
  24. """
  25. self.settings = settings
  26. self.quiet = quiet
  27. def heading(self, text: str, style: str | None = None) -> None:
  28. """Display a standardized heading.
  29. Args:
  30. text: Heading text
  31. style: Optional style override (defaults to STYLE_HEADER from settings)
  32. """
  33. if style is None:
  34. style = self.settings.STYLE_HEADER
  35. console.print(f"[{style}]{text}[/{style}]")
  36. console.print("") # Add newline after heading
  37. def text(self, text: str, style: str | None = None) -> None:
  38. """Display plain text with optional styling.
  39. Args:
  40. text: Text to display
  41. style: Optional Rich style markup
  42. """
  43. if style:
  44. console.print(f"[{style}]{text}[/{style}]")
  45. else:
  46. console.print(text)
  47. def table(
  48. self,
  49. headers: list[str] | None = None,
  50. rows: list[tuple] | None = None,
  51. title: str | None = None,
  52. show_header: bool = True,
  53. borderless: bool = False,
  54. ) -> None:
  55. """Display a standardized table.
  56. Args:
  57. headers: Column headers (if None, no headers)
  58. rows: List of tuples, one per row
  59. title: Optional table title
  60. show_header: Whether to show header row
  61. borderless: If True, use borderless style (box=None)
  62. """
  63. table = Table(
  64. title=title,
  65. show_header=show_header and headers is not None,
  66. header_style=self.settings.STYLE_TABLE_HEADER,
  67. box=None,
  68. padding=self.settings.PADDING_TABLE_NORMAL if borderless else (0, 1),
  69. )
  70. # Add columns
  71. if headers:
  72. for header in headers:
  73. table.add_column(header)
  74. elif rows and len(rows) > 0:
  75. # No headers, but need columns for data
  76. for _ in range(len(rows[0])):
  77. table.add_column()
  78. # Add rows
  79. if rows:
  80. for row in rows:
  81. table.add_row(*[str(cell) for cell in row])
  82. console.print(table)
  83. def tree(self, root_label: str, nodes: dict | list | Tree) -> None:
  84. """Display a tree structure.
  85. Args:
  86. root_label: Label for the root node
  87. nodes: Hierarchical structure (dict, list, or pre-built Tree)
  88. """
  89. if isinstance(nodes, Tree):
  90. console.print(nodes)
  91. else:
  92. tree = Tree(root_label)
  93. self._build_tree_nodes(tree, nodes)
  94. console.print(tree)
  95. def _build_tree_nodes(self, parent, nodes):
  96. """Recursively build tree nodes.
  97. Args:
  98. parent: Parent tree node
  99. nodes: Dict or list of child nodes
  100. """
  101. if isinstance(nodes, dict):
  102. for key, value in nodes.items():
  103. if isinstance(value, (dict, list)):
  104. branch = parent.add(str(key))
  105. self._build_tree_nodes(branch, value)
  106. else:
  107. parent.add(f"{key}: {value}")
  108. elif isinstance(nodes, list):
  109. for item in nodes:
  110. if isinstance(item, (dict, list)):
  111. self._build_tree_nodes(parent, item)
  112. else:
  113. parent.add(str(item))
  114. def _print_tree(self, tree) -> None:
  115. """Print a pre-built Rich Tree object.
  116. Args:
  117. tree: Rich Tree object to print
  118. """
  119. console.print(tree)
  120. def _print_table(self, table) -> None:
  121. """Print a pre-built Rich Table object.
  122. Enforces consistent header styling for all tables.
  123. Args:
  124. table: Rich Table object to print
  125. """
  126. # Enforce consistent header style for all tables
  127. table.header_style = self.settings.STYLE_TABLE_HEADER
  128. console.print(table)
  129. def _print_markdown(self, markdown) -> None:
  130. """Print a pre-built Rich Markdown object.
  131. Args:
  132. markdown: Rich Markdown object to print
  133. """
  134. console.print(markdown)
  135. def code(self, code_text: str, language: str | None = None) -> None:
  136. """Display code with optional syntax highlighting.
  137. Args:
  138. code_text: Code to display
  139. language: Programming language for syntax highlighting
  140. """
  141. if language:
  142. syntax = Syntax(code_text, language, theme="monokai", line_numbers=False)
  143. console.print(syntax)
  144. else:
  145. # Plain code block without highlighting
  146. console.print(f"[dim]{code_text}[/dim]")
  147. def progress(self, *columns):
  148. """Create a Rich Progress context manager with standardized console.
  149. Args:
  150. *columns: Progress columns (e.g., SpinnerColumn(), TextColumn())
  151. Returns:
  152. Progress context manager
  153. Example:
  154. with display.progress(
  155. SpinnerColumn(), TextColumn("[progress.description]{task.description}")
  156. ) as progress:
  157. task = progress.add_task("Processing...", total=None)
  158. # do work
  159. progress.remove_task(task)
  160. """
  161. return Progress(*columns, console=console)
  162. # ===== Formatting Utilities =====
  163. def truncate(self, value: str, max_length: int | None = None) -> str:
  164. """Truncate a string value if it exceeds maximum length.
  165. Args:
  166. value: String value to truncate
  167. max_length: Maximum length (uses default if None)
  168. Returns:
  169. Truncated string with suffix if needed
  170. """
  171. if max_length is None:
  172. max_length = self.settings.VALUE_MAX_LENGTH_DEFAULT
  173. if max_length > 0 and len(value) > max_length:
  174. return value[: max_length - len(self.settings.TRUNCATION_SUFFIX)] + self.settings.TRUNCATION_SUFFIX
  175. return value
  176. def format_file_size(self, size_bytes: int) -> str:
  177. """Format file size in human-readable format (B, KB, MB).
  178. Args:
  179. size_bytes: Size in bytes
  180. Returns:
  181. Formatted size string (e.g., "1.5KB", "2.3MB")
  182. """
  183. if size_bytes < self.settings.SIZE_KB_THRESHOLD:
  184. return f"{size_bytes}B"
  185. if size_bytes < self.settings.SIZE_MB_THRESHOLD:
  186. kb = size_bytes / self.settings.SIZE_KB_THRESHOLD
  187. return f"{kb:.{self.settings.SIZE_DECIMAL_PLACES}f}KB"
  188. mb = size_bytes / self.settings.SIZE_MB_THRESHOLD
  189. return f"{mb:.{self.settings.SIZE_DECIMAL_PLACES}f}MB"
  190. def file_tree(
  191. self,
  192. root_label: str,
  193. files: list,
  194. file_info_fn: callable,
  195. title: str | None = None,
  196. ) -> None:
  197. """Display a file tree structure.
  198. Args:
  199. root_label: Label for root node (e.g., "📁 my-project")
  200. files: List of file items to display
  201. file_info_fn: Function that takes a file and returns
  202. (path, display_name, color, extra_text) where:
  203. - path: Path object for directory structure
  204. - display_name: Name to show for the file
  205. - color: Rich color for the filename
  206. - extra_text: Optional additional text
  207. title: Optional heading to display before tree
  208. """
  209. if title:
  210. self.heading(title)
  211. tree = Tree(root_label)
  212. tree_nodes = {Path(): tree}
  213. for file_item in sorted(files, key=lambda f: file_info_fn(f)[0]):
  214. path, display_name, color, extra_text = file_info_fn(file_item)
  215. parts = path.parts
  216. current_path = Path()
  217. current_node = tree
  218. # Build directory structure
  219. for part in parts[:-1]:
  220. current_path = current_path / part
  221. if current_path not in tree_nodes:
  222. new_node = current_node.add(f"{IconManager.folder()} [white]{part}[/white]")
  223. tree_nodes[current_path] = new_node
  224. current_node = tree_nodes[current_path]
  225. # Add file
  226. icon = IconManager.get_file_icon(display_name)
  227. file_label = f"{icon} [{color}]{display_name}[/{color}]"
  228. if extra_text:
  229. file_label += f" {extra_text}"
  230. current_node.add(file_label)
  231. console.print(tree)
  232. def _get_icon_by_type(self, icon_type: str) -> str:
  233. """Get icon by semantic type name.
  234. Args:
  235. icon_type: Type of icon (e.g., 'folder', 'file', 'config', 'lock')
  236. Returns:
  237. Icon unicode character
  238. """
  239. icon_map = {
  240. "folder": IconManager.folder(),
  241. "file": IconManager.FILE_DEFAULT,
  242. "config": IconManager.config(),
  243. "lock": IconManager.lock(),
  244. "arrow": IconManager.arrow_right(),
  245. }
  246. return icon_map.get(icon_type, "")
  247. def get_lock_icon(self) -> str:
  248. """Get the lock icon for sensitive variables.
  249. Returns:
  250. Lock icon unicode character
  251. """
  252. return IconManager.lock()