prompt.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. """Simplified prompt handler for template variables."""
  2. from typing import Dict, Any, List, Tuple, Optional
  3. from collections import OrderedDict
  4. from rich.console import Console
  5. from rich.prompt import Prompt, Confirm, IntPrompt, FloatPrompt
  6. import logging
  7. from .variables import TemplateVariable
  8. logger = logging.getLogger('boilerplates')
  9. console = Console()
  10. class SimplifiedPromptHandler:
  11. """Prompt handler for template-detected variables."""
  12. def __init__(self, variables: Dict[str, TemplateVariable]):
  13. """Initialize with template variables.
  14. Args:
  15. variables: Dict of variable name to TemplateVariable object
  16. """
  17. self.variables = variables
  18. self.values = {}
  19. self.category_metadata = {} # Will be set by Module if available
  20. def __call__(self) -> Dict[str, Any]:
  21. """Execute the prompting flow."""
  22. # Group variables by prefix (preserves registration order)
  23. groups, standalone = self._group_variables()
  24. # Process standalone variables first as "General" group
  25. if standalone:
  26. self._process_variable_set("General", standalone, is_group=False)
  27. # Process each group in the order they were registered
  28. for group_name, group_vars in groups.items():
  29. self._process_variable_set(group_name.title(), group_vars, is_group=True)
  30. return self.values
  31. def _group_variables(self) -> Tuple[Dict[str, List[str]], List[str]]:
  32. """Group variables by their prefix or enabler status.
  33. Returns:
  34. (groups, standalone) where groups is {prefix: [var_names]}
  35. """
  36. groups = OrderedDict()
  37. standalone = []
  38. # First pass: identify all groups
  39. for var_name, var in self.variables.items():
  40. if var.group:
  41. # This variable belongs to a group
  42. if var.group not in groups:
  43. groups[var.group] = []
  44. groups[var.group].append(var_name)
  45. else:
  46. # Check if this is an enabler for other variables
  47. # An enabler is a variable that other variables use as their group
  48. is_group_enabler = any(v.group == var_name for v in self.variables.values())
  49. if is_group_enabler:
  50. if var_name not in groups:
  51. groups[var_name] = []
  52. # The enabler itself is not added to the group list
  53. else:
  54. # Truly standalone variable
  55. standalone.append(var_name)
  56. return groups, standalone
  57. def _process_variable_set(self, display_name: str, var_names: List[str],
  58. is_group: bool = False):
  59. """Unified method to process any set of variables.
  60. Args:
  61. display_name: Name to show in the header
  62. var_names: List of variable names to process
  63. is_group: Whether this is a group (vs standalone)
  64. """
  65. # Deduplicate variables
  66. var_names = list(dict.fromkeys(var_names)) # Preserves order while removing duplicates
  67. # Get icon for this category
  68. icon = self._get_category_icon(display_name)
  69. # Check if this group has an enabler
  70. group_name = display_name.lower()
  71. enabler = None
  72. if is_group and group_name in self.variables:
  73. enabler_var = self.variables[group_name]
  74. if enabler_var.is_enabler:
  75. enabler = group_name
  76. # Show section header with icon
  77. console.print(f"\n{icon}[bold cyan]{display_name}[/bold cyan]")
  78. enabled = Confirm.ask(
  79. f"Enable {enabler}?",
  80. default=bool(enabler_var.default)
  81. )
  82. self.values[enabler] = enabled
  83. if not enabled:
  84. # Skip all group variables
  85. return
  86. # Split into required and optional
  87. required = []
  88. optional = []
  89. for var_name in var_names:
  90. var = self.variables[var_name]
  91. if var.is_required:
  92. required.append(var_name)
  93. else:
  94. optional.append(var_name)
  95. # Apply defaults
  96. for var_name in optional:
  97. self.values[var_name] = self.variables[var_name].default
  98. # Process required variables
  99. if required:
  100. if not enabler: # Show header only if we haven't shown it for enabler
  101. console.print(f"\n{icon}[bold cyan]{display_name}[/bold cyan]")
  102. for var_name in required:
  103. var = self.variables[var_name]
  104. self.values[var_name] = self._prompt_variable(var, required=True)
  105. # Process optional variables
  106. if optional:
  107. # Filter out enabler variables from display
  108. display_optional = [v for v in optional if v != enabler]
  109. if display_optional:
  110. # Show section header if not already shown
  111. if not required and not enabler:
  112. console.print(f"\n{icon}[bold cyan]{display_name}[/bold cyan]")
  113. # Show current values
  114. self._show_variables_inline(display_optional)
  115. if Confirm.ask("Do you want to change any values?", default=False):
  116. for var_name in optional:
  117. # Skip the enabler variable as it was already handled
  118. if var_name == enabler:
  119. continue
  120. var = self.variables[var_name]
  121. self.values[var_name] = self._prompt_variable(
  122. var, current_value=self.values[var_name]
  123. )
  124. def _show_variables_inline(self, var_names: List[str]):
  125. """Display variables inline without header."""
  126. items = []
  127. for var_name in var_names:
  128. var = self.variables[var_name]
  129. value = self.values.get(var_name, var.default)
  130. if value is not None:
  131. # Format value based on type
  132. if isinstance(value, bool):
  133. formatted_value = str(value).lower()
  134. elif isinstance(value, str) and ' ' in value:
  135. formatted_value = f"'{value}'"
  136. else:
  137. formatted_value = str(value)
  138. items.append(f"{var.display_name}: {formatted_value}")
  139. if items:
  140. console.print(f"[dim white]{', '.join(items)}[/dim white]")
  141. def _get_category_icon(self, category: str) -> str:
  142. """Get icon for a category."""
  143. # Only use icons from metadata
  144. if self.category_metadata and category.lower() in self.category_metadata:
  145. cat_meta = self.category_metadata[category.lower()]
  146. if 'icon' in cat_meta:
  147. return cat_meta['icon'] + ' '
  148. return '' # No icon if not defined in metadata
  149. def _prompt_variable(
  150. self,
  151. var: TemplateVariable,
  152. required: bool = False,
  153. current_value: Any = None
  154. ) -> Any:
  155. """Prompt for a single variable value."""
  156. # Build prompt message with description if available
  157. display_text = var.description if var.description else var.display_name
  158. # Add hint if available
  159. hint_text = ""
  160. if var.hint:
  161. hint_text = f" [dim]({var.hint})[/dim]"
  162. # Build the full prompt
  163. if current_value is not None:
  164. prompt_msg = f"Enter {display_text}{hint_text} [dim]({current_value})[/dim]"
  165. elif required:
  166. prompt_msg = f"Enter {display_text}{hint_text} [red](Required)[/red]"
  167. else:
  168. prompt_msg = f"Enter {display_text}{hint_text}"
  169. # Show tip if available
  170. if var.tip:
  171. console.print(f"[dim cyan]💡 {var.tip}[/dim cyan]")
  172. # Handle different types
  173. if var.type == 'boolean':
  174. default = bool(current_value) if current_value is not None else None
  175. return Confirm.ask(prompt_msg, default=default)
  176. elif var.type == 'integer':
  177. default = int(current_value) if current_value is not None else None
  178. while True:
  179. try:
  180. return IntPrompt.ask(prompt_msg, default=default)
  181. except ValueError:
  182. console.print("[red]Please enter a valid integer[/red]")
  183. elif var.type == 'float':
  184. default = float(current_value) if current_value is not None else None
  185. while True:
  186. try:
  187. return FloatPrompt.ask(prompt_msg, default=default)
  188. except ValueError:
  189. console.print("[red]Please enter a valid number[/red]")
  190. else: # string
  191. default = str(current_value) if current_value is not None else None
  192. while True:
  193. value = Prompt.ask(prompt_msg, default=default) or ""
  194. if required and not value.strip():
  195. console.print("[red]This field is required[/red]")
  196. continue
  197. return value.strip()