display_variable.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. from __future__ import annotations
  2. import logging
  3. from typing import TYPE_CHECKING
  4. from rich.table import Table
  5. from .display_icons import IconManager
  6. from .display_settings import DisplaySettings
  7. logger = logging.getLogger(__name__)
  8. if TYPE_CHECKING:
  9. from ..template import Template
  10. from .display_base import BaseDisplay
  11. class VariableDisplay:
  12. """Variable-related rendering.
  13. Provides methods for displaying variables, sections,
  14. and their values with appropriate formatting based on context.
  15. """
  16. def __init__(self, settings: DisplaySettings, base: BaseDisplay):
  17. """Initialize VariableDisplay.
  18. Args:
  19. settings: Display settings for formatting
  20. base: BaseDisplay instance
  21. """
  22. self.settings = settings
  23. self.base = base
  24. def render_variable_value(
  25. self,
  26. variable,
  27. _context: str = "default",
  28. is_dimmed: bool = False,
  29. var_satisfied: bool = True,
  30. ) -> str:
  31. """Render variable value with appropriate formatting based on context.
  32. Args:
  33. variable: Variable instance to render
  34. _context: Display context (unused, kept for API compatibility)
  35. is_dimmed: Whether the variable should be dimmed
  36. var_satisfied: Whether the variable's dependencies are satisfied
  37. Returns:
  38. Formatted string representation of the variable value
  39. """
  40. # Handle disabled bool variables
  41. if (is_dimmed or not var_satisfied) and variable.type == "bool":
  42. if hasattr(variable, "_original_disabled") and variable._original_disabled is not False:
  43. return f"{variable._original_disabled} {IconManager.arrow_right()} False"
  44. return "False"
  45. # Handle config overrides with arrow
  46. if (
  47. variable.origin == "config"
  48. and hasattr(variable, "_original_stored")
  49. and variable.original_value != variable.value
  50. ):
  51. settings = self.settings
  52. orig = self._format_value(
  53. variable,
  54. variable.original_value,
  55. max_length=settings.VALUE_MAX_LENGTH_SHORT,
  56. )
  57. curr = variable.get_display_value(
  58. mask_secret=True,
  59. max_length=settings.VALUE_MAX_LENGTH_SHORT,
  60. show_none=False,
  61. )
  62. if not curr:
  63. curr = str(variable.value) if variable.value else settings.TEXT_EMPTY_OVERRIDE
  64. arrow = IconManager.arrow_right()
  65. color = settings.COLOR_WARNING
  66. return f"[dim]{orig}[/dim] [bold {color}]{arrow} {curr}[/bold {color}]"
  67. # Default formatting
  68. settings = self.settings
  69. value = variable.get_display_value(
  70. mask_secret=True,
  71. max_length=settings.VALUE_MAX_LENGTH_DEFAULT,
  72. show_none=True,
  73. )
  74. if not variable.value:
  75. return f"[{settings.COLOR_MUTED}]{value}[/{settings.COLOR_MUTED}]"
  76. return value
  77. def _format_value(self, variable, value, max_length: int | None = None) -> str:
  78. """Helper to format a specific value.
  79. Args:
  80. variable: Variable instance
  81. value: Value to format
  82. max_length: Maximum length before truncation
  83. Returns:
  84. Formatted value string
  85. """
  86. settings = self.settings
  87. if variable.is_secret():
  88. return settings.SENSITIVE_MASK
  89. if value is None or value == "":
  90. return f"[{settings.COLOR_MUTED}]({settings.TEXT_EMPTY_VALUE})[/{settings.COLOR_MUTED}]"
  91. val_str = str(value)
  92. return self.base.truncate(val_str, max_length)
  93. def render_section(self, title: str, description: str | None) -> None:
  94. """Display a section header.
  95. Args:
  96. title: Section title
  97. description: Optional section description
  98. """
  99. settings = self.settings
  100. if description:
  101. self.base.text(
  102. f"\n{title} - {description}",
  103. style=f"{settings.STYLE_SECTION_TITLE} {settings.STYLE_SECTION_DESC}",
  104. )
  105. else:
  106. self.base.text(f"\n{title}", style=settings.STYLE_SECTION_TITLE)
  107. self.base.text(
  108. settings.SECTION_SEPARATOR_CHAR * settings.SECTION_SEPARATOR_LENGTH,
  109. style=settings.COLOR_MUTED,
  110. )
  111. def _render_section_header(self, section, is_dimmed: bool) -> str:
  112. """Build section header text with appropriate styling.
  113. Args:
  114. section: VariableSection instance
  115. is_dimmed: Whether section is dimmed (disabled)
  116. Returns:
  117. Formatted header text with Rich markup
  118. """
  119. settings = self.settings
  120. # Show (disabled) label if section has a toggle and is not enabled
  121. disabled_text = settings.LABEL_DISABLED if (section.toggle and not section.is_enabled()) else ""
  122. if is_dimmed:
  123. style = settings.STYLE_DISABLED
  124. return f"[bold {style}]{section.title}{disabled_text}[/bold {style}]"
  125. return f"[bold]{section.title}{disabled_text}[/bold]"
  126. def _render_variable_options(self, variable) -> str:
  127. """Render compact variable options/configuration summary."""
  128. parts: list[str] = []
  129. config = variable.config
  130. if variable.type == "enum" and variable.options:
  131. parts.append(", ".join(variable.options))
  132. if config:
  133. parts.extend(self._render_textarea_option(variable, config))
  134. parts.extend(self._render_integer_options(variable, config))
  135. parts.extend(self._render_secret_options(variable))
  136. return self.base.truncate(" | ".join(parts), self.settings.VALUE_MAX_LENGTH_DEFAULT) if parts else ""
  137. @staticmethod
  138. def _render_textarea_option(variable, config) -> list[str]:
  139. if variable.type in {"str", "secret"} and config.textarea:
  140. return ["textarea"]
  141. return []
  142. @staticmethod
  143. def _render_integer_options(variable, config) -> list[str]:
  144. if variable.type != "int":
  145. return []
  146. parts: list[str] = []
  147. if config.slider and config.min is not None and config.max is not None:
  148. slider_part = f"slider {config.min}..{config.max}"
  149. step = config.step if config.step is not None else 1
  150. if step != 1:
  151. slider_part += f" step {step}"
  152. parts.append(slider_part)
  153. else:
  154. bounds = []
  155. if config.min is not None:
  156. bounds.append(f"min={config.min}")
  157. if config.max is not None:
  158. bounds.append(f"max={config.max}")
  159. if config.step is not None:
  160. bounds.append(f"step={config.step}")
  161. if bounds:
  162. parts.append(" ".join(bounds))
  163. if config.unit:
  164. parts.append(f"unit={config.unit}")
  165. return parts
  166. @staticmethod
  167. def _render_secret_options(variable) -> list[str]:
  168. if not (variable.is_secret() and variable.autogenerated):
  169. return []
  170. if variable.autogenerated_base64:
  171. bytes_value = (
  172. variable.autogenerated_config.bytes_or_default()
  173. if variable.autogenerated_config
  174. else variable.autogenerated_length
  175. )
  176. return [f"autogen base64 bytes={bytes_value}"]
  177. length = (
  178. variable.autogenerated_config.length_or_default()
  179. if variable.autogenerated_config
  180. else variable.autogenerated_length
  181. )
  182. parts = [f"autogen chars len={length}"]
  183. if variable.autogenerated_config and variable.autogenerated_config.characters:
  184. parts.append(f"charset={len(variable.autogenerated_config.characters)}")
  185. return parts
  186. def _render_variable_row(self, var_name: str, variable, is_dimmed: bool, var_satisfied: bool) -> tuple:
  187. """Build variable row data for table display.
  188. Args:
  189. var_name: Variable name
  190. variable: Variable instance
  191. is_dimmed: Whether containing section is dimmed
  192. var_satisfied: Whether variable dependencies are satisfied
  193. Returns:
  194. Tuple of (var_display, type, default_val, options, description, row_style)
  195. """
  196. settings = self.settings
  197. # Build row style
  198. row_style = settings.STYLE_DISABLED if (is_dimmed or not var_satisfied) else None
  199. # Build default value
  200. default_val = self.render_variable_value(variable, is_dimmed=is_dimmed, var_satisfied=var_satisfied)
  201. # Build variable display name
  202. secret_icon = f" {IconManager.lock()}" if variable.is_secret() else ""
  203. # Only show required indicator if variable is enabled (not dimmed and dependencies satisfied)
  204. required_indicator = settings.LABEL_REQUIRED if variable.required and not is_dimmed and var_satisfied else ""
  205. var_display = f"{settings.VAR_NAME_INDENT}{var_name}{secret_icon}{required_indicator}"
  206. return (
  207. var_display,
  208. variable.type or "str",
  209. default_val,
  210. self._render_variable_options(variable),
  211. variable.description or "",
  212. row_style,
  213. )
  214. def render_variables_table(self, template: Template) -> None:
  215. """Display a table of variables for a template.
  216. All variables and sections are always shown. Disabled sections/variables
  217. are displayed with dimmed styling.
  218. Args:
  219. template: Template instance
  220. """
  221. if not (template.variables and template.variables.has_sections()):
  222. return
  223. settings = self.settings
  224. self.base.text("")
  225. self.base.heading("Template Variables")
  226. variables_table = Table(show_header=True, header_style=settings.STYLE_TABLE_HEADER)
  227. variables_table.add_column("Variable", style=settings.STYLE_VAR_COL_NAME, no_wrap=True)
  228. variables_table.add_column("Type", style=settings.STYLE_VAR_COL_TYPE)
  229. variables_table.add_column("Default", style=settings.STYLE_VAR_COL_DEFAULT)
  230. variables_table.add_column("Options", style=settings.STYLE_VAR_COL_DEFAULT)
  231. variables_table.add_column("Description", style=settings.STYLE_VAR_COL_DESC)
  232. first_section = True
  233. for section in template.variables.get_sections().values():
  234. if not section.variables:
  235. continue
  236. if not first_section:
  237. variables_table.add_row("", "", "", "", "", style=settings.STYLE_DISABLED)
  238. first_section = False
  239. # Check if section is enabled AND dependencies are satisfied
  240. is_enabled = section.is_enabled()
  241. dependencies_satisfied = template.variables.is_section_satisfied(section.key)
  242. is_dimmed = not (is_enabled and dependencies_satisfied)
  243. # Render section header
  244. header_text = self._render_section_header(section, is_dimmed)
  245. variables_table.add_row(header_text, "", "", "", "")
  246. # Render variables
  247. for var_name, variable in section.variables.items():
  248. # Check if variable's needs are satisfied
  249. var_satisfied = template.variables.is_variable_satisfied(var_name)
  250. # Build and add row
  251. (
  252. var_display,
  253. var_type,
  254. default_val,
  255. options,
  256. description,
  257. row_style,
  258. ) = self._render_variable_row(var_name, variable, is_dimmed, var_satisfied)
  259. variables_table.add_row(var_display, var_type, default_val, options, description, style=row_style)
  260. self.base._print_table(variables_table)