| """Contains the base class :class:`.BaseListPrompt` which can be used to create a prompt involving choices.""" |
| from abc import abstractmethod |
| from typing import Any, Callable, List, Optional |
|
|
| from prompt_toolkit.filters.base import Condition |
| from prompt_toolkit.keys import Keys |
|
|
| from InquirerPy.base.complex import BaseComplexPrompt |
| from InquirerPy.base.control import InquirerPyUIListControl |
| from InquirerPy.separator import Separator |
| from InquirerPy.utils import ( |
| InquirerPyKeybindings, |
| InquirerPyMessage, |
| InquirerPySessionResult, |
| InquirerPyStyle, |
| InquirerPyValidate, |
| ) |
|
|
|
|
| class BaseListPrompt(BaseComplexPrompt): |
| """A base class to create a complex prompt involving choice selections (i.e. list) using `prompt_toolkit` 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. |
| |
| See Also: |
| :class:`~InquirerPy.prompts.list.ListPrompt` |
| :class:`~InquirerPy.prompts.fuzzy.FuzzyPrompt` |
| """ |
|
|
| def __init__( |
| self, |
| message: InquirerPyMessage, |
| style: Optional[InquirerPyStyle] = None, |
| vi_mode: bool = False, |
| qmark: str = "?", |
| amark: str = "?", |
| instruction: str = "", |
| long_instruction: str = "", |
| border: bool = False, |
| transformer: Optional[Callable[[Any], Any]] = None, |
| filter: Optional[Callable[[Any], Any]] = None, |
| validate: Optional[InquirerPyValidate] = None, |
| invalid_message: str = "Invalid input", |
| multiselect: bool = False, |
| keybindings: Optional[InquirerPyKeybindings] = None, |
| cycle: bool = True, |
| 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, |
| border=border, |
| vi_mode=vi_mode, |
| qmark=qmark, |
| amark=amark, |
| transformer=transformer, |
| filter=filter, |
| invalid_message=invalid_message, |
| validate=validate, |
| instruction=instruction, |
| long_instruction=long_instruction, |
| wrap_lines=wrap_lines, |
| raise_keyboard_interrupt=raise_keyboard_interrupt, |
| mandatory=mandatory, |
| mandatory_message=mandatory_message, |
| session_result=session_result, |
| ) |
|
|
| self._content_control: InquirerPyUIListControl |
| self._multiselect = multiselect |
| self._is_multiselect = Condition(lambda: self._multiselect) |
| self._cycle = cycle |
|
|
| if not keybindings: |
| keybindings = {} |
|
|
| self.kb_maps = { |
| "down": [ |
| {"key": "down"}, |
| {"key": "c-n", "filter": ~self._is_vim_edit}, |
| {"key": "j", "filter": self._is_vim_edit}, |
| ], |
| "up": [ |
| {"key": "up"}, |
| {"key": "c-p", "filter": ~self._is_vim_edit}, |
| {"key": "k", "filter": self._is_vim_edit}, |
| ], |
| "toggle": [ |
| {"key": "space"}, |
| ], |
| "toggle-down": [ |
| {"key": Keys.Tab}, |
| ], |
| "toggle-up": [ |
| {"key": Keys.BackTab}, |
| ], |
| "toggle-all": [ |
| {"key": "alt-r"}, |
| {"key": "c-r"}, |
| ], |
| "toggle-all-true": [ |
| {"key": "alt-a"}, |
| {"key": "c-a"}, |
| ], |
| "toggle-all-false": [], |
| **keybindings, |
| } |
|
|
| self.kb_func_lookup = { |
| "down": [{"func": self._handle_down}], |
| "up": [{"func": self._handle_up}], |
| "toggle": [{"func": self._handle_toggle_choice}], |
| "toggle-down": [ |
| {"func": self._handle_toggle_choice}, |
| {"func": self._handle_down}, |
| ], |
| "toggle-up": [ |
| {"func": self._handle_toggle_choice}, |
| {"func": self._handle_up}, |
| ], |
| "toggle-all": [{"func": self._handle_toggle_all}], |
| "toggle-all-true": [{"func": self._handle_toggle_all, "args": [True]}], |
| "toggle-all-false": [{"func": self._handle_toggle_all, "args": [False]}], |
| } |
|
|
| @property |
| def content_control(self) -> InquirerPyUIListControl: |
| """Get the content controller object. |
| |
| Needs to be an instance of :class:`~InquirerPy.base.control.InquirerPyUIListControl`. |
| |
| Each :class:`.BaseComplexPrompt` requires a `content_control` to display custom |
| contents for the prompt. |
| |
| Raises: |
| NotImplementedError: When `self._content_control` is not found. |
| """ |
| if not self._content_control: |
| raise NotImplementedError |
| return self._content_control |
|
|
| @content_control.setter |
| def content_control(self, value: InquirerPyUIListControl) -> None: |
| self._content_control = value |
|
|
| @property |
| def result_name(self) -> Any: |
| """Get the result value that should be printed to the terminal. |
| |
| In multiselect scenario, return result as a list. |
| """ |
| if self._multiselect: |
| return [choice["name"] for choice in self.selected_choices] |
| else: |
| try: |
| return self.content_control.selection["name"] |
| except IndexError: |
| return "" |
|
|
| @property |
| def result_value(self) -> Any: |
| """Get the result value that should return to the user. |
| |
| In multiselect scenario, return result as a list. |
| """ |
| if self._multiselect: |
| return [choice["value"] for choice in self.selected_choices] |
| else: |
| try: |
| return self.content_control.selection["value"] |
| except IndexError: |
| return "" |
|
|
| @property |
| def selected_choices(self) -> List[Any]: |
| """List[Any]: Get all user selected choices.""" |
|
|
| def filter_choice(choice): |
| return not isinstance(choice, Separator) and choice["enabled"] |
|
|
| return list(filter(filter_choice, self.content_control.choices)) |
|
|
| def _handle_down(self, _) -> bool: |
| """Handle event when user attempts to move down. |
| |
| Returns: |
| Boolean indicating if the action hits the cap. |
| """ |
| if self._cycle: |
| self.content_control.selected_choice_index = ( |
| self.content_control.selected_choice_index + 1 |
| ) % self.content_control.choice_count |
| return False |
| else: |
| self.content_control.selected_choice_index += 1 |
| if ( |
| self.content_control.selected_choice_index |
| >= self.content_control.choice_count |
| ): |
| self.content_control.selected_choice_index = ( |
| self.content_control.choice_count - 1 |
| ) |
| return True |
| return False |
|
|
| def _handle_up(self, _) -> bool: |
| """Handle event when user attempts to move up. |
| |
| Returns: |
| Boolean indicating if the action hits the cap. |
| """ |
| if self._cycle: |
| self.content_control.selected_choice_index = ( |
| self.content_control.selected_choice_index - 1 |
| ) % self.content_control.choice_count |
| return False |
| else: |
| self.content_control.selected_choice_index -= 1 |
| if self.content_control.selected_choice_index < 0: |
| self.content_control.selected_choice_index = 0 |
| return True |
| return False |
|
|
| @abstractmethod |
| def _handle_toggle_choice(self, event) -> None: |
| """Handle event when user attempting to toggle the state of the chocie.""" |
| pass |
|
|
| @abstractmethod |
| def _handle_toggle_all(self, event, value: bool) -> None: |
| """Handle event when user attempting to alter the state of all choices.""" |
| pass |
|
|