Spaces:
Sleeping
Sleeping
| """Contains the interface class :class:`.BaseComplexPrompt` for more complex prompts and the mocked document class :class:`.FakeDocument`.""" | |
| import shutil | |
| from dataclasses import dataclass | |
| from typing import Any, Callable, List, Optional, Tuple, Union | |
| from prompt_toolkit.application import Application | |
| from prompt_toolkit.enums import EditingMode | |
| from prompt_toolkit.filters.base import Condition, FilterOrBool | |
| from prompt_toolkit.key_binding.key_bindings import KeyHandlerCallable | |
| from prompt_toolkit.keys import Keys | |
| from InquirerPy.base.simple import BaseSimplePrompt | |
| from InquirerPy.enum import INQUIRERPY_KEYBOARD_INTERRUPT | |
| from InquirerPy.utils import ( | |
| InquirerPySessionResult, | |
| InquirerPyStyle, | |
| InquirerPyValidate, | |
| ) | |
| class FakeDocument: | |
| """A fake `prompt_toolkit` document class. | |
| Work around to allow non-buffer type :class:`~prompt_toolkit.layout.UIControl` to use | |
| :class:`~prompt_toolkit.validation.Validator`. | |
| Args: | |
| text: Content to be validated. | |
| cursor_position: Fake cursor position. | |
| """ | |
| text: str | |
| cursor_position: int = 0 | |
| class BaseComplexPrompt(BaseSimplePrompt): | |
| """A base class to create a more complex prompt that will involve :class:`~prompt_toolkit.application.Application`. | |
| Note: | |
| This class does not create :class:`~prompt_toolkit.layout.Layout` nor :class:`~prompt_toolkit.application.Application`, | |
| it only contains the necessary attributes and helper functions to be consumed. | |
| Note: | |
| Use :class:`~InquirerPy.base.BaseListPrompt` to create a complex list prompt which involves multiple choices. It has | |
| more methods and helper function implemented. | |
| See Also: | |
| :class:`~InquirerPy.base.BaseListPrompt` | |
| :class:`~InquirerPy.prompts.fuzzy.FuzzyPrompt` | |
| """ | |
| def __init__( | |
| self, | |
| message: Union[str, Callable[[InquirerPySessionResult], str]], | |
| style: Optional[InquirerPyStyle] = None, | |
| border: bool = False, | |
| vi_mode: bool = False, | |
| qmark: str = "?", | |
| amark: str = "?", | |
| instruction: str = "", | |
| long_instruction: str = "", | |
| transformer: Optional[Callable[[Any], Any]] = None, | |
| filter: Optional[Callable[[Any], Any]] = None, | |
| validate: Optional[InquirerPyValidate] = None, | |
| invalid_message: str = "Invalid input", | |
| wrap_lines: bool = True, | |
| raise_keyboard_interrupt: bool = True, | |
| mandatory: bool = True, | |
| mandatory_message: str = "Mandatory prompt", | |
| session_result: Optional[InquirerPySessionResult] = None, | |
| ) -> None: | |
| super().__init__( | |
| message=message, | |
| style=style, | |
| vi_mode=vi_mode, | |
| qmark=qmark, | |
| amark=amark, | |
| instruction=instruction, | |
| transformer=transformer, | |
| filter=filter, | |
| invalid_message=invalid_message, | |
| validate=validate, | |
| wrap_lines=wrap_lines, | |
| raise_keyboard_interrupt=raise_keyboard_interrupt, | |
| mandatory=mandatory, | |
| mandatory_message=mandatory_message, | |
| session_result=session_result, | |
| ) | |
| self._invalid_message = invalid_message | |
| self._rendered = False | |
| self._invalid = False | |
| self._loading = False | |
| self._application: Application | |
| self._long_instruction = long_instruction | |
| self._border = border | |
| self._height_offset = 2 # prev prompt result + current prompt question | |
| if self._border: | |
| self._height_offset += 2 | |
| if self._long_instruction: | |
| self._height_offset += 1 | |
| self._validation_window_bottom_offset = 0 if not self._long_instruction else 1 | |
| if self._wrap_lines: | |
| self._validation_window_bottom_offset += ( | |
| self.extra_long_instruction_line_count | |
| ) | |
| self._is_vim_edit = Condition(lambda: self._editing_mode == EditingMode.VI) | |
| self._is_invalid = Condition(lambda: self._invalid) | |
| self._is_displaying_long_instruction = Condition( | |
| lambda: self._long_instruction != "" | |
| ) | |
| def _redraw(self) -> None: | |
| """Redraw the application UI.""" | |
| self._application.invalidate() | |
| def register_kb( | |
| self, *keys: Union[Keys, str], filter: FilterOrBool = True | |
| ) -> Callable[[KeyHandlerCallable], KeyHandlerCallable]: | |
| """Decorate keybinding registration function. | |
| Ensure that the `invalid` state is cleared on next keybinding entered. | |
| """ | |
| kb_dec = super().register_kb(*keys, filter=filter) | |
| def decorator(func: KeyHandlerCallable) -> KeyHandlerCallable: | |
| def executable(event): | |
| if self._invalid: | |
| self._invalid = False | |
| func(event) | |
| return executable | |
| return decorator | |
| def _exception_handler(self, _, context) -> None: | |
| """Set exception handler for the event loop. | |
| Skip the question and raise exception. | |
| Args: | |
| loop: Current event loop. | |
| context: Exception context. | |
| """ | |
| self._status["answered"] = True | |
| self._status["result"] = INQUIRERPY_KEYBOARD_INTERRUPT | |
| self._status["skipped"] = True | |
| self._application.exit(exception=context["exception"]) | |
| def _after_render(self, app: Optional[Application]) -> None: | |
| """Run after the :class:`~prompt_toolkit.application.Application` is rendered/updated. | |
| Since this function is fired up on each render, adding a check on `self._rendered` to | |
| process logics that should only run once. | |
| Set event loop exception handler here, since its guaranteed that the event loop is running | |
| in `_after_render`. | |
| """ | |
| if not self._rendered: | |
| self._rendered = True | |
| self._keybinding_factory() | |
| self._on_rendered(app) | |
| def _set_error(self, message: str) -> None: | |
| """Set error message and set invalid state. | |
| Args: | |
| message: Error message to display. | |
| """ | |
| self._invalid_message = message | |
| self._invalid = True | |
| def _get_error_message(self) -> List[Tuple[str, str]]: | |
| """Obtain the error message dynamically. | |
| Returns: | |
| FormattedText in list of tuple format. | |
| """ | |
| return [ | |
| ( | |
| "class:validation-toolbar", | |
| self._invalid_message, | |
| ) | |
| ] | |
| def _on_rendered(self, _: Optional[Application]) -> None: | |
| """Run once after the UI is rendered. Acts like `ComponentDidMount`.""" | |
| pass | |
| def _get_prompt_message(self) -> List[Tuple[str, str]]: | |
| """Get the prompt message to display. | |
| Returns: | |
| Formatted text in list of tuple format. | |
| """ | |
| pre_answer = ( | |
| "class:instruction", | |
| " %s " % self.instruction if self.instruction else " ", | |
| ) | |
| post_answer = ("class:answer", " %s" % self.status["result"]) | |
| return super()._get_prompt_message(pre_answer, post_answer) | |
| def _run(self) -> Any: | |
| """Run the application.""" | |
| return self.application.run() | |
| async def _run_async(self) -> None: | |
| """Run the application asynchronously.""" | |
| return await self.application.run_async() | |
| def application(self) -> Application: | |
| """Get the application. | |
| :class:`.BaseComplexPrompt` requires :attr:`.BaseComplexPrompt._application` to be defined since this class | |
| doesn't implement :class:`~prompt_toolkit.layout.Layout` and :class:`~prompt_toolkit.application.Application`. | |
| Raises: | |
| NotImplementedError: When `self._application` is not defined. | |
| """ | |
| if not self._application: | |
| raise NotImplementedError | |
| return self._application | |
| def application(self, value: Application) -> None: | |
| self._application = value | |
| def height_offset(self) -> int: | |
| """int: Height offset to apply.""" | |
| if not self._wrap_lines: | |
| return self._height_offset | |
| return self.extra_line_count + self._height_offset | |
| def total_message_length(self) -> int: | |
| """int: Total length of the message.""" | |
| total_message_length = 0 | |
| if self._qmark: | |
| total_message_length += len(self._qmark) | |
| total_message_length += 1 # Extra space if qmark is present | |
| total_message_length += len(str(self._message)) | |
| total_message_length += 1 # Extra space between message and instruction | |
| total_message_length += len(str(self._instruction)) | |
| if self._instruction: | |
| total_message_length += 1 # Extra space behind the instruction | |
| return total_message_length | |
| def extra_message_line_count(self) -> int: | |
| """int: Get the extra lines created caused by line wrapping. | |
| Minus 1 on the totoal message length as we only want the extra line. | |
| 24 // 24 will equal to 1 however we only want the value to be 1 when we have 25 char | |
| which will create an extra line. | |
| """ | |
| term_width, _ = shutil.get_terminal_size() | |
| return (self.total_message_length - 1) // term_width | |
| def extra_long_instruction_line_count(self) -> int: | |
| """int: Get the extra lines created caused by line wrapping. | |
| See Also: | |
| :attr:`.BaseComplexPrompt.extra_message_line_count` | |
| """ | |
| if self._long_instruction: | |
| term_width, _ = shutil.get_terminal_size() | |
| return (len(self._long_instruction) - 1) // term_width | |
| else: | |
| return 0 | |
| def extra_line_count(self) -> int: | |
| """Get the extra lines created caused by line wrapping. | |
| Used mainly to calculate how much additional offset should be applied when getting | |
| the height. | |
| Returns: | |
| Total extra lines created due to line wrapping. | |
| """ | |
| result = 0 | |
| # message wrap | |
| result += self.extra_message_line_count | |
| # long instruction wrap | |
| result += self.extra_long_instruction_line_count | |
| return result | |