|
"""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 |
|
|