input_manager.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. """Input Manager for standardized user input handling.
  2. This module provides a centralized interface for all user input operations,
  3. ensuring consistent styling and validation across the CLI.
  4. """
  5. from __future__ import annotations
  6. import logging
  7. import re
  8. from typing import Callable
  9. from rich.console import Console
  10. from rich.prompt import Confirm, IntPrompt, Prompt
  11. from .input_settings import InputSettings
  12. logger = logging.getLogger(__name__)
  13. console = Console()
  14. class InputManager:
  15. """Manages all user input operations with standardized styling.
  16. This class provides primitives for various types of user input including
  17. text, passwords, confirmations, choices, and validated inputs.
  18. """
  19. def __init__(self, settings: InputSettings | None = None):
  20. """Initialize InputManager.
  21. Args:
  22. settings: Input configuration settings (uses default if None)
  23. """
  24. self.settings = settings or InputSettings()
  25. def text(
  26. self,
  27. prompt: str,
  28. default: str | None = None,
  29. password: bool = False,
  30. validator: Callable[[str], bool] | None = None,
  31. error_message: str | None = None,
  32. ) -> str:
  33. """Prompt for text input.
  34. Args:
  35. prompt: Prompt message to display
  36. default: Default value if user presses Enter
  37. password: If True, mask the input
  38. validator: Optional validation function
  39. error_message: Custom error message for validation failure
  40. Returns:
  41. User input string
  42. """
  43. if password:
  44. return self.password(prompt, default)
  45. while True:
  46. result = Prompt.ask(
  47. f"[{self.settings.PROMPT_STYLE}]{prompt}[/{self.settings.PROMPT_STYLE}]",
  48. default=default or "",
  49. console=console,
  50. )
  51. if validator and not validator(result):
  52. msg = error_message or "Invalid input"
  53. console.print(f"[{self.settings.PROMPT_ERROR_STYLE}]{msg}[/{self.settings.PROMPT_ERROR_STYLE}]")
  54. continue
  55. return result
  56. def password(self, prompt: str, default: str | None = None) -> str:
  57. """Prompt for password input (masked).
  58. Args:
  59. prompt: Prompt message to display
  60. default: Default value if user presses Enter
  61. Returns:
  62. User input string (masked during entry)
  63. """
  64. return Prompt.ask(
  65. f"[{self.settings.PROMPT_STYLE}]{prompt}[/{self.settings.PROMPT_STYLE}]",
  66. default=default or "",
  67. password=True,
  68. console=console,
  69. )
  70. def confirm(self, prompt: str, default: bool | None = None) -> bool:
  71. """Prompt for yes/no confirmation.
  72. Args:
  73. prompt: Prompt message to display
  74. default: Default value if user presses Enter
  75. Returns:
  76. True for yes, False for no
  77. """
  78. if default is None:
  79. default = self.settings.DEFAULT_CONFIRM_YES
  80. return Confirm.ask(
  81. f"[{self.settings.PROMPT_STYLE}]{prompt}[/{self.settings.PROMPT_STYLE}]",
  82. default=default,
  83. console=console,
  84. )
  85. def integer(
  86. self,
  87. prompt: str,
  88. default: int | None = None,
  89. min_value: int | None = None,
  90. max_value: int | None = None,
  91. ) -> int:
  92. """Prompt for integer input with optional range validation.
  93. Args:
  94. prompt: Prompt message to display
  95. default: Default value if user presses Enter
  96. min_value: Minimum allowed value
  97. max_value: Maximum allowed value
  98. Returns:
  99. Integer value
  100. """
  101. while True:
  102. if default is not None:
  103. result = IntPrompt.ask(
  104. f"[{self.settings.PROMPT_STYLE}]{prompt}[/{self.settings.PROMPT_STYLE}]",
  105. default=default,
  106. console=console,
  107. )
  108. else:
  109. try:
  110. result = IntPrompt.ask(
  111. f"[{self.settings.PROMPT_STYLE}]{prompt}[/{self.settings.PROMPT_STYLE}]",
  112. console=console,
  113. )
  114. except ValueError:
  115. console.print(
  116. f"[{self.settings.PROMPT_ERROR_STYLE}]{self.settings.MSG_INVALID_INTEGER}[/{self.settings.PROMPT_ERROR_STYLE}]"
  117. )
  118. continue
  119. # Validate range
  120. if min_value is not None and result < min_value:
  121. error_style = self.settings.PROMPT_ERROR_STYLE
  122. console.print(f"[{error_style}]Value must be at least {min_value}[/{error_style}]")
  123. continue
  124. if max_value is not None and result > max_value:
  125. error_style = self.settings.PROMPT_ERROR_STYLE
  126. console.print(f"[{error_style}]Value must be at most {max_value}[/{error_style}]")
  127. continue
  128. return result
  129. def choice(self, prompt: str, choices: list[str], default: str | None = None) -> str:
  130. """Prompt user to select one option from a list.
  131. Args:
  132. prompt: Prompt message to display
  133. choices: List of valid options
  134. default: Default choice if user presses Enter
  135. Returns:
  136. Selected choice
  137. """
  138. if not choices:
  139. raise ValueError("Choices list cannot be empty")
  140. choices_display = f"[{', '.join(choices)}]"
  141. full_prompt = f"{prompt} {choices_display}"
  142. while True:
  143. result = Prompt.ask(
  144. f"[{self.settings.PROMPT_STYLE}]{full_prompt}[/{self.settings.PROMPT_STYLE}]",
  145. default=default or "",
  146. console=console,
  147. )
  148. if result in choices:
  149. return result
  150. console.print(
  151. f"[{self.settings.PROMPT_ERROR_STYLE}]{self.settings.MSG_INVALID_CHOICE}[/{self.settings.PROMPT_ERROR_STYLE}]"
  152. )
  153. def validate_email(self, email: str) -> bool:
  154. """Validate email address format.
  155. Args:
  156. email: Email address to validate
  157. Returns:
  158. True if valid, False otherwise
  159. """
  160. pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
  161. return bool(re.match(pattern, email))
  162. def validate_url(self, url: str) -> bool:
  163. """Validate URL format.
  164. Args:
  165. url: URL to validate
  166. Returns:
  167. True if valid, False otherwise
  168. """
  169. pattern = r"^https?://[^\s/$.?#].[^\s]*$"
  170. return bool(re.match(pattern, url, re.IGNORECASE))
  171. def validate_hostname(self, hostname: str) -> bool:
  172. """Validate hostname/domain format.
  173. Args:
  174. hostname: Hostname to validate
  175. Returns:
  176. True if valid, False otherwise
  177. """
  178. pattern = (
  179. r"^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*"
  180. r"[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$"
  181. )
  182. return bool(re.match(pattern, hostname))