prompt.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. from __future__ import annotations
  2. from typing import Dict, Any, List, Callable
  3. import logging
  4. from rich.console import Console
  5. from rich.prompt import Prompt, Confirm, IntPrompt
  6. from rich.table import Table
  7. from .variables import Variable, VariableCollection
  8. logger = logging.getLogger(__name__)
  9. # ---------------------------
  10. # SECTION: PromptHandler Class
  11. # ---------------------------
  12. class PromptHandler:
  13. """Simple interactive prompt handler for collecting template variables."""
  14. def __init__(self) -> None:
  15. self.console = Console()
  16. # --------------------------
  17. # SECTION: Public Methods
  18. # --------------------------
  19. def collect_variables(self, variables: VariableCollection) -> dict[str, Any]:
  20. """Collect values for variables by iterating through sections.
  21. Args:
  22. variables: VariableCollection with organized sections and variables
  23. Returns:
  24. Dict of variable names to collected values
  25. """
  26. if not Confirm.ask("Customize any settings?", default=False):
  27. logger.info("User opted to keep all default values")
  28. return {}
  29. collected: Dict[str, Any] = {}
  30. # Process each section
  31. for section_key, section in variables._set.items():
  32. if not section.variables:
  33. continue
  34. # Always show section header first
  35. self.console.print(f"\n[bold cyan]{section.title}[/bold cyan]")
  36. if section.description:
  37. self.console.print(f"[dim]{section.description}[/dim]")
  38. self.console.print("─" * 40, style="dim")
  39. # Handle section toggle - skip for required sections
  40. if section.required:
  41. # Required sections are always processed, no toggle prompt needed
  42. logger.debug(f"Processing required section '{section.key}' without toggle prompt")
  43. elif section.toggle:
  44. toggle_var = section.variables.get(section.toggle)
  45. if toggle_var:
  46. prompt_text = section.prompt or f"Enable {section.title}?"
  47. current_value = toggle_var.get_typed_value()
  48. new_value = self._prompt_bool(prompt_text, current_value)
  49. if new_value != current_value:
  50. collected[toggle_var.name] = new_value
  51. toggle_var.value = new_value
  52. # Skip remaining variables in section if disabled
  53. if not new_value:
  54. continue
  55. # Collect variables in this section
  56. for var_name, variable in section.variables.items():
  57. # Skip toggle variable (already handled)
  58. if section.toggle and var_name == section.toggle:
  59. continue
  60. current_value = variable.get_typed_value()
  61. new_value = self._prompt_variable(variable)
  62. if new_value != current_value:
  63. collected[var_name] = new_value
  64. variable.value = new_value
  65. logger.info(f"Variable collection completed. Collected {len(collected)} values")
  66. return collected
  67. # !SECTION
  68. # ---------------------------
  69. # SECTION: Private Methods
  70. # ---------------------------
  71. def _prompt_variable(self, variable: Variable) -> Any:
  72. """Prompt for a single variable value based on its type."""
  73. logger.debug(f"Prompting for variable '{variable.name}' (type: {variable.type})")
  74. prompt_text = variable.prompt or variable.description or variable.name
  75. # Friendly hint for common semantic types
  76. if variable.type in ["hostname", "email", "url"]:
  77. prompt_text += f" ({variable.type})"
  78. try:
  79. default_value = variable.get_typed_value()
  80. except ValueError:
  81. default_value = variable.value
  82. handler = self._get_prompt_handler(variable)
  83. while True:
  84. try:
  85. raw = handler(prompt_text, default_value)
  86. return variable.convert(raw)
  87. except ValueError as exc:
  88. self._show_validation_error(str(exc))
  89. except Exception as e:
  90. logger.error(f"Error prompting for variable '{variable.name}': {str(e)}")
  91. default_value = variable.value
  92. handler = self._get_prompt_handler(variable)
  93. def _get_prompt_handler(self, variable: Variable) -> Callable:
  94. """Return the prompt function for a variable type."""
  95. handlers = {
  96. "bool": self._prompt_bool,
  97. "int": self._prompt_int,
  98. "enum": lambda text, default: self._prompt_enum(text, variable.options or [], default),
  99. }
  100. return handlers.get(variable.type, self._prompt_string)
  101. def _show_validation_error(self, message: str) -> None:
  102. """Display validation feedback consistently."""
  103. self.console.print(f"[red]{message}[/red]")
  104. def _prompt_string(self, prompt_text: str, default: Any = None) -> str:
  105. value = Prompt.ask(
  106. prompt_text,
  107. default=str(default) if default is not None else "",
  108. show_default=True,
  109. )
  110. return value.strip() if value else ""
  111. def _prompt_bool(self, prompt_text: str, default: Any = None) -> bool:
  112. default_bool = None
  113. if default is not None:
  114. default_bool = default if isinstance(default, bool) else str(default).lower() in ("true", "1", "yes", "on")
  115. return Confirm.ask(prompt_text, default=default_bool)
  116. def _prompt_int(self, prompt_text: str, default: Any = None) -> int:
  117. default_int = None
  118. if default is not None:
  119. try:
  120. default_int = int(default)
  121. except (ValueError, TypeError):
  122. logger.warning(f"Invalid default integer value: {default}")
  123. return IntPrompt.ask(prompt_text, default=default_int)
  124. def _prompt_enum(self, prompt_text: str, options: list[str], default: Any = None) -> str:
  125. """Prompt for enum selection with validation."""
  126. if not options:
  127. return self._prompt_string(prompt_text, default)
  128. self.console.print(f" Options: {', '.join(options)}", style="dim")
  129. # Validate default is in options
  130. if default and str(default) not in options:
  131. default = options[0]
  132. while True:
  133. value = Prompt.ask(
  134. prompt_text,
  135. default=str(default) if default else options[0],
  136. show_default=True,
  137. )
  138. if value in options:
  139. return value
  140. self.console.print(f"[red]Invalid choice. Select from: {', '.join(options)}[/red]")
  141. # !SECTION