from __future__ import annotations from typing import TYPE_CHECKING, Any, Optional, Protocol from ._input_handler import TextInputHandler from .element import CursorOffset, Element if TYPE_CHECKING: from .styles.base import BaseStyle class Validator(Protocol): """Protocol for validators that can validate input values. Any object with a validate_python method can be used as a validator. This includes Pydantic's TypeAdapter or custom validators. Example with Pydantic TypeAdapter: >>> from pydantic import TypeAdapter >>> validator = TypeAdapter(int) >>> input_field = Input(validator=validator) Example with custom validator: >>> class MyValidator: ... def validate_python(self, value): ... if not value.startswith("x"): ... raise ValueError("Must start with x") ... return value >>> input_field = Input(validator=MyValidator()) """ def validate_python(self, value: Any) -> Any: """Validate a Python value and return the validated result. Args: value: The value to validate Returns: The validated value Raises: ValidationError: If validation fails """ ... class Input(TextInputHandler, Element): label: Optional[str] = None def __init__( self, label: Optional[str] = None, placeholder: Optional[str] = None, default: Optional[str] = None, default_as_placeholder: bool = True, required: bool = False, required_message: Optional[str] = None, password: bool = False, inline: bool = False, name: Optional[str] = None, style: Optional[BaseStyle] = None, validator: Optional[Validator] = None, **metadata: Any, ): self.name = name self.label = label self._placeholder = placeholder self.default = default self.default_as_placeholder = default_as_placeholder self.required = required self.password = password self.inline = inline self.text = "" self.valid = None self.required_message = required_message self._validation_message: Optional[str] = None self._validator: Optional[Validator] = validator Element.__init__(self, style=style, metadata=metadata) super().__init__() @property def placeholder(self) -> str: if self.default_as_placeholder and self.default: return self.default return self._placeholder or "" @property def validation_message(self) -> Optional[str]: if self._validation_message: return self._validation_message assert self.valid return None @property def cursor_offset(self) -> CursorOffset: top = 1 if self.inline else 2 left_offset = 0 if self.inline and self.label: left_offset = len(self.label) + 1 return CursorOffset(top=top, left=self.cursor_left + left_offset) @property def should_show_cursor(self) -> bool: return True def on_blur(self): self.on_validate() def on_validate(self): value = self.value.strip() if not value and self.required: self.valid = False self._validation_message = self.required_message or "This field is required" return if self._validator: from pydantic import ValidationError try: self._validator.validate_python(value) except ValidationError as e: self.valid = False # Extract error message from Pydantic ValidationError self._validation_message = e.errors()[0].get("msg", "Validation failed") return self._validation_message = None self.valid = True @property def value(self) -> str: return self.text or self.default or "" def ask(self) -> str: from .container import Container container = Container(style=self.style) container.elements = [self] container.run() return self.value