| | """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, |
| | ) |
| |
|
| |
|
| | @dataclass |
| | 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 |
| | 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: |
| | @kb_dec |
| | 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() |
| |
|
| | @property |
| | 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 |
| |
|
| | @application.setter |
| | def application(self, value: Application) -> None: |
| | self._application = value |
| |
|
| | @property |
| | 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 |
| |
|
| | @property |
| | 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 |
| | total_message_length += len(str(self._message)) |
| | total_message_length += 1 |
| | total_message_length += len(str(self._instruction)) |
| | if self._instruction: |
| | total_message_length += 1 |
| | return total_message_length |
| |
|
| | @property |
| | 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 |
| |
|
| | @property |
| | 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 |
| |
|
| | @property |
| | 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 |
| |
|
| | |
| | result += self.extra_message_line_count |
| | |
| | result += self.extra_long_instruction_line_count |
| |
|
| | return result |
| |
|