input_manager.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  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(
  54. f"[{self.settings.PROMPT_ERROR_STYLE}]{msg}[/{self.settings.PROMPT_ERROR_STYLE}]"
  55. )
  56. continue
  57. return result
  58. def password(self, prompt: str, default: str | None = None) -> str:
  59. """Prompt for password input (masked).
  60. Args:
  61. prompt: Prompt message to display
  62. default: Default value if user presses Enter
  63. Returns:
  64. User input string (masked during entry)
  65. """
  66. return Prompt.ask(
  67. f"[{self.settings.PROMPT_STYLE}]{prompt}[/{self.settings.PROMPT_STYLE}]",
  68. default=default or "",
  69. password=True,
  70. console=console,
  71. )
  72. def confirm(self, prompt: str, default: bool | None = None) -> bool:
  73. """Prompt for yes/no confirmation.
  74. Args:
  75. prompt: Prompt message to display
  76. default: Default value if user presses Enter
  77. Returns:
  78. True for yes, False for no
  79. """
  80. if default is None:
  81. default = self.settings.DEFAULT_CONFIRM_YES
  82. return Confirm.ask(
  83. f"[{self.settings.PROMPT_STYLE}]{prompt}[/{self.settings.PROMPT_STYLE}]",
  84. default=default,
  85. console=console,
  86. )
  87. def integer(
  88. self,
  89. prompt: str,
  90. default: int | None = None,
  91. min_value: int | None = None,
  92. max_value: int | None = None,
  93. ) -> int:
  94. """Prompt for integer input with optional range validation.
  95. Args:
  96. prompt: Prompt message to display
  97. default: Default value if user presses Enter
  98. min_value: Minimum allowed value
  99. max_value: Maximum allowed value
  100. Returns:
  101. Integer value
  102. """
  103. while True:
  104. if default is not None:
  105. result = IntPrompt.ask(
  106. f"[{self.settings.PROMPT_STYLE}]{prompt}[/{self.settings.PROMPT_STYLE}]",
  107. default=default,
  108. console=console,
  109. )
  110. else:
  111. try:
  112. result = IntPrompt.ask(
  113. f"[{self.settings.PROMPT_STYLE}]{prompt}[/{self.settings.PROMPT_STYLE}]",
  114. console=console,
  115. )
  116. except ValueError:
  117. console.print(
  118. f"[{self.settings.PROMPT_ERROR_STYLE}]{self.settings.MSG_INVALID_INTEGER}[/{self.settings.PROMPT_ERROR_STYLE}]"
  119. )
  120. continue
  121. # Validate range
  122. if min_value is not None and result < min_value:
  123. console.print(
  124. f"[{self.settings.PROMPT_ERROR_STYLE}]Value must be at least {min_value}[/{self.settings.PROMPT_ERROR_STYLE}]"
  125. )
  126. continue
  127. if max_value is not None and result > max_value:
  128. console.print(
  129. f"[{self.settings.PROMPT_ERROR_STYLE}]Value must be at most {max_value}[/{self.settings.PROMPT_ERROR_STYLE}]"
  130. )
  131. continue
  132. return result
  133. def choice(
  134. self, prompt: str, choices: list[str], default: str | None = None
  135. ) -> str:
  136. """Prompt user to select one option from a list.
  137. Args:
  138. prompt: Prompt message to display
  139. choices: List of valid options
  140. default: Default choice if user presses Enter
  141. Returns:
  142. Selected choice
  143. """
  144. if not choices:
  145. raise ValueError("Choices list cannot be empty")
  146. choices_display = f"[{', '.join(choices)}]"
  147. full_prompt = f"{prompt} {choices_display}"
  148. while True:
  149. result = Prompt.ask(
  150. f"[{self.settings.PROMPT_STYLE}]{full_prompt}[/{self.settings.PROMPT_STYLE}]",
  151. default=default or "",
  152. console=console,
  153. )
  154. if result in choices:
  155. return result
  156. console.print(
  157. f"[{self.settings.PROMPT_ERROR_STYLE}]{self.settings.MSG_INVALID_CHOICE}[/{self.settings.PROMPT_ERROR_STYLE}]"
  158. )
  159. def validate_email(self, email: str) -> bool:
  160. """Validate email address format.
  161. Args:
  162. email: Email address to validate
  163. Returns:
  164. True if valid, False otherwise
  165. """
  166. pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
  167. return bool(re.match(pattern, email))
  168. def validate_url(self, url: str) -> bool:
  169. """Validate URL format.
  170. Args:
  171. url: URL to validate
  172. Returns:
  173. True if valid, False otherwise
  174. """
  175. pattern = r"^https?://[^\s/$.?#].[^\s]*$"
  176. return bool(re.match(pattern, url, re.IGNORECASE))
  177. def validate_hostname(self, hostname: str) -> bool:
  178. """Validate hostname/domain format.
  179. Args:
  180. hostname: Hostname to validate
  181. Returns:
  182. True if valid, False otherwise
  183. """
  184. pattern = r"^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$"
  185. return bool(re.match(pattern, hostname))