display_base.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  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 code(self, code_text: str, language: str | None = None) -> None:
  130. """Display code with optional syntax highlighting.
  131. Args:
  132. code_text: Code to display
  133. language: Programming language for syntax highlighting
  134. """
  135. if language:
  136. syntax = Syntax(code_text, language, theme="monokai", line_numbers=False)
  137. console.print(syntax)
  138. else:
  139. # Plain code block without highlighting
  140. console.print(f"[dim]{code_text}[/dim]")
  141. def progress(self, *columns):
  142. """Create a Rich Progress context manager with standardized console.
  143. Args:
  144. *columns: Progress columns (e.g., SpinnerColumn(), TextColumn())
  145. Returns:
  146. Progress context manager
  147. Example:
  148. with display.progress(
  149. SpinnerColumn(), TextColumn("[progress.description]{task.description}")
  150. ) as progress:
  151. task = progress.add_task("Processing...", total=None)
  152. # do work
  153. progress.remove_task(task)
  154. """
  155. return Progress(*columns, console=console)
  156. # ===== Formatting Utilities =====
  157. def truncate(self, value: str, max_length: int | None = None) -> str:
  158. """Truncate a string value if it exceeds maximum length.
  159. Args:
  160. value: String value to truncate
  161. max_length: Maximum length (uses default if None)
  162. Returns:
  163. Truncated string with suffix if needed
  164. """
  165. if max_length is None:
  166. max_length = self.settings.VALUE_MAX_LENGTH_DEFAULT
  167. if max_length > 0 and len(value) > max_length:
  168. return value[: max_length - len(self.settings.TRUNCATION_SUFFIX)] + self.settings.TRUNCATION_SUFFIX
  169. return value
  170. def format_file_size(self, size_bytes: int) -> str:
  171. """Format file size in human-readable format (B, KB, MB).
  172. Args:
  173. size_bytes: Size in bytes
  174. Returns:
  175. Formatted size string (e.g., "1.5KB", "2.3MB")
  176. """
  177. if size_bytes < self.settings.SIZE_KB_THRESHOLD:
  178. return f"{size_bytes}B"
  179. if size_bytes < self.settings.SIZE_MB_THRESHOLD:
  180. kb = size_bytes / self.settings.SIZE_KB_THRESHOLD
  181. return f"{kb:.{self.settings.SIZE_DECIMAL_PLACES}f}KB"
  182. mb = size_bytes / self.settings.SIZE_MB_THRESHOLD
  183. return f"{mb:.{self.settings.SIZE_DECIMAL_PLACES}f}MB"
  184. def file_tree(
  185. self,
  186. root_label: str,
  187. files: list,
  188. file_info_fn: callable,
  189. title: str | None = None,
  190. ) -> None:
  191. """Display a file tree structure.
  192. Args:
  193. root_label: Label for root node (e.g., "📁 my-project")
  194. files: List of file items to display
  195. file_info_fn: Function that takes a file and returns
  196. (path, display_name, color, extra_text) where:
  197. - path: Path object for directory structure
  198. - display_name: Name to show for the file
  199. - color: Rich color for the filename
  200. - extra_text: Optional additional text
  201. title: Optional heading to display before tree
  202. """
  203. if title:
  204. self.heading(title)
  205. tree = Tree(root_label)
  206. tree_nodes = {Path(): tree}
  207. for file_item in sorted(files, key=lambda f: file_info_fn(f)[0]):
  208. path, display_name, color, extra_text = file_info_fn(file_item)
  209. parts = path.parts
  210. current_path = Path()
  211. current_node = tree
  212. # Build directory structure
  213. for part in parts[:-1]:
  214. current_path = current_path / part
  215. if current_path not in tree_nodes:
  216. new_node = current_node.add(f"{IconManager.folder()} [white]{part}[/white]")
  217. tree_nodes[current_path] = new_node
  218. current_node = tree_nodes[current_path]
  219. # Add file
  220. icon = IconManager.get_file_icon(display_name)
  221. file_label = f"{icon} [{color}]{display_name}[/{color}]"
  222. if extra_text:
  223. file_label += f" {extra_text}"
  224. current_node.add(file_label)
  225. console.print(tree)
  226. def _get_icon_by_type(self, icon_type: str) -> str:
  227. """Get icon by semantic type name.
  228. Args:
  229. icon_type: Type of icon (e.g., 'folder', 'file', 'config', 'lock')
  230. Returns:
  231. Icon unicode character
  232. """
  233. icon_map = {
  234. "folder": IconManager.folder(),
  235. "file": IconManager.FILE_DEFAULT,
  236. "config": IconManager.config(),
  237. "lock": IconManager.lock(),
  238. "arrow": IconManager.arrow_right(),
  239. }
  240. return icon_map.get(icon_type, "")
  241. def get_lock_icon(self) -> str:
  242. """Get the lock icon for sensitive variables.
  243. Returns:
  244. Lock icon unicode character
  245. """
  246. return IconManager.lock()