From 5f1a610dad94ac2760d5075992f043f4226993e3 Mon Sep 17 00:00:00 2001 From: Kevin Zhuang Date: Mon, 14 Jun 2021 22:45:10 +1000 Subject: [PATCH] feat: allow disable cycle in list type prompts #9 --- InquirerPy/base.py | 33 +++++++++++++++++++++++++------ InquirerPy/prompts/checkbox.py | 3 +++ InquirerPy/prompts/expand.py | 27 ++++++++++++++++++------- InquirerPy/prompts/fuzzy/fuzzy.py | 3 +++ InquirerPy/prompts/list.py | 3 +++ InquirerPy/prompts/rawlist.py | 3 +++ examples/example_checkbox.py | 3 +++ examples/example_expand.py | 3 ++- 8 files changed, 64 insertions(+), 14 deletions(-) diff --git a/InquirerPy/base.py b/InquirerPy/base.py index 3250478..cdba508 100644 --- a/InquirerPy/base.py +++ b/InquirerPy/base.py @@ -666,12 +666,16 @@ def application(self, value: Application) -> None: """Setter for `self._application`.""" self._application = value - def _handle_down(self) -> None: - """Handle event when user attempting to move down.""" + def _handle_down(self) -> bool: + """Handle event when user attempting to move down. + + :return: 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 ( @@ -681,17 +685,25 @@ def _handle_down(self) -> None: self.content_control.selected_choice_index = ( self.content_control.choice_count - 1 ) + return True + return False - def _handle_up(self) -> None: - """Handle event when user attempting to move down.""" + def _handle_up(self) -> bool: + """Handle event when user attempting to move down. + + :return: 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_enter(self, event) -> None: @@ -730,6 +742,7 @@ class BaseListPrompt(BaseComplexPrompt): :param invalid_message: Message to display when input is invalid. :param multiselect: Enable multiselect mode. :param keybindings: Custom keybindings to apply. + :param cycle: Return to top item if hit bottom or vice versa. :param show_cursor: Display cursor at the end of the prompt. """ @@ -855,16 +868,24 @@ def _toggle_all(self, value: bool = None) -> None: def _handle_up(self) -> None: """Handle the event when user attempt to move up.""" while True: - super()._handle_up() + cap = super()._handle_up() if not isinstance(self.content_control.selection["value"], Separator): break + else: + if cap and not self._cycle: + self._handle_down() + break def _handle_down(self) -> None: """Handle the event when user attempt to move down.""" while True: - super()._handle_down() + cap = super()._handle_down() if not isinstance(self.content_control.selection["value"], Separator): break + else: + if cap and not self._cycle: + self._handle_up() + break def _handle_enter(self, event) -> None: """Handle the event when user hit Enter key. diff --git a/InquirerPy/prompts/checkbox.py b/InquirerPy/prompts/checkbox.py index f174691..a02eae9 100644 --- a/InquirerPy/prompts/checkbox.py +++ b/InquirerPy/prompts/checkbox.py @@ -102,6 +102,7 @@ class CheckboxPrompt(BaseListPrompt): :param invalid_message: Message to display when input is invalid. :param keybindings: Custom keybindings to apply. :param show_cursor: Display cursor at the end of the prompt. + :param cycle: Return to top item if hit bottom or vice versa. """ def __init__( @@ -124,6 +125,7 @@ def __init__( invalid_message: str = "Invalid input", keybindings: Dict[str, List[Dict[str, Any]]] = None, show_cursor: bool = True, + cycle: bool = True, session_result: SessionResult = None, ) -> None: """Initialise the content_control and create Application.""" @@ -150,6 +152,7 @@ def __init__( multiselect=True, keybindings=keybindings, show_cursor=show_cursor, + cycle=cycle, session_result=session_result, ) diff --git a/InquirerPy/prompts/expand.py b/InquirerPy/prompts/expand.py index e59dcca..4bf0170 100644 --- a/InquirerPy/prompts/expand.py +++ b/InquirerPy/prompts/expand.py @@ -3,7 +3,7 @@ from prompt_toolkit.validation import Validator -from InquirerPy.base import BaseListPrompt, InquirerPyUIControl +from InquirerPy.base import BaseComplexPrompt, BaseListPrompt, InquirerPyUIControl from InquirerPy.enum import INQUIRERPY_POINTER_SEQUENCE from InquirerPy.exceptions import InvalidArgument, RequiredKeyNotFound from InquirerPy.separator import Separator @@ -175,6 +175,7 @@ class ExpandPrompt(BaseListPrompt): :param invalid_message: Message to display when input is invalid. :param keybindings: Custom keybindings to apply. :param show_cursor: Display cursor at the end of the prompt. + :param cycle: Return to top item if hit bottom or vice versa. """ def __init__( @@ -201,6 +202,7 @@ def __init__( invalid_message: str = "Invalid input", keybindings: Dict[str, List[Dict[str, Any]]] = None, show_cursor: bool = True, + cycle: bool = True, session_result: SessionResult = None, ) -> None: """Create the application and apply keybindings.""" @@ -231,6 +233,7 @@ def __init__( multiselect=multiselect, keybindings=keybindings, show_cursor=show_cursor, + cycle=cycle, session_result=session_result, ) @@ -270,13 +273,15 @@ def _handle_up(self) -> None: if not self.content_control._expanded: return while True: - self.content_control.selected_choice_index = ( - self.content_control.selected_choice_index - 1 - ) % self.content_control.choice_count + cap = BaseComplexPrompt._handle_up(self) if not isinstance( self.content_control.selection["value"], Separator ) and not isinstance(self.content_control.selection["value"], ExpandHelp): break + else: + if cap and not self._cycle: + self._handle_down() + break def _handle_down(self) -> None: """Handle the event when user attempt to move down. @@ -286,13 +291,21 @@ def _handle_down(self) -> None: if not self.content_control._expanded: return while True: - self.content_control.selected_choice_index = ( - self.content_control.selected_choice_index + 1 - ) % self.content_control.choice_count + cap = BaseComplexPrompt._handle_down(self) if not isinstance( self.content_control.selection["value"], Separator ) and not isinstance(self.content_control.selection["value"], ExpandHelp): break + elif ( + isinstance(self.content_control.selection["value"], ExpandHelp) + and not self._cycle + ): + self._handle_up() + break + else: + if cap and not self._cycle: + self._handle_up() + break @property def instruction(self) -> str: diff --git a/InquirerPy/prompts/fuzzy/fuzzy.py b/InquirerPy/prompts/fuzzy/fuzzy.py index 889e7fb..74eb312 100644 --- a/InquirerPy/prompts/fuzzy/fuzzy.py +++ b/InquirerPy/prompts/fuzzy/fuzzy.py @@ -254,6 +254,7 @@ class FuzzyPrompt(BaseComplexPrompt): :param validate: A callable or Validator instance to validate user selection. :param invalid_message: Message to display when input is invalid. :param keybindings: Custom keybindings to apply. + :param cycle: Return to top item if hit bottom or vice versa. """ def __init__( @@ -279,6 +280,7 @@ def __init__( validate: Union[Callable[[Any], bool], Validator] = None, invalid_message: str = "Invalid input", keybindings: Dict[str, List[Dict[str, Any]]] = None, + cycle: bool = True, session_result: SessionResult = None, ) -> None: if not keybindings: @@ -307,6 +309,7 @@ def __init__( multiselect=multiselect, instruction=instruction, keybindings=keybindings, + cycle=cycle, session_result=session_result, ) self._default = default if not isinstance(default, Callable) else default(self._result) # type: ignore diff --git a/InquirerPy/prompts/list.py b/InquirerPy/prompts/list.py index 8bc6550..484dcda 100644 --- a/InquirerPy/prompts/list.py +++ b/InquirerPy/prompts/list.py @@ -95,6 +95,7 @@ class ListPrompt(BaseListPrompt): :param invalid_message: Message to display when input is invalid. :param keybindings: Custom keybindings to apply. :param show_cursor: Display cursor at the end of the prompt. + :param cycle: Return to top item if hit bottom or vice versa. """ def __init__( @@ -118,6 +119,7 @@ def __init__( invalid_message: str = "Invalid input", keybindings: Dict[str, List[Dict[str, Any]]] = None, show_cursor: bool = True, + cycle: bool = True, session_result: SessionResult = None, ) -> None: """Initialise the content_control and create Application.""" @@ -146,5 +148,6 @@ def __init__( multiselect=multiselect, keybindings=keybindings, show_cursor=show_cursor, + cycle=cycle, session_result=session_result, ) diff --git a/InquirerPy/prompts/rawlist.py b/InquirerPy/prompts/rawlist.py index ba715d6..e8054f4 100644 --- a/InquirerPy/prompts/rawlist.py +++ b/InquirerPy/prompts/rawlist.py @@ -129,6 +129,7 @@ class RawlistPrompt(BaseListPrompt): :param invalid_message: Message to display when input is invalid. :param keybindings: Custom keybindings to apply. :param show_cursor: Display cursor at the end of the prompt. + :param cycle: Return to top item if hit bottom or vice versa. """ def __init__( @@ -153,6 +154,7 @@ def __init__( invalid_message: str = "Invalid input", keybindings: Dict[str, List[Dict[str, Any]]] = None, show_cursor: bool = True, + cycle: bool = True, session_result: SessionResult = None, ) -> None: """Construct content control and initialise the application while also apply keybindings.""" @@ -182,6 +184,7 @@ def __init__( invalid_message=invalid_message, keybindings=keybindings, show_cursor=show_cursor, + cycle=cycle, session_result=session_result, ) diff --git a/examples/example_checkbox.py b/examples/example_checkbox.py index e5c96f2..e75f731 100644 --- a/examples/example_checkbox.py +++ b/examples/example_checkbox.py @@ -4,11 +4,13 @@ def question1_choice(_): return [ + Separator(), {"name": "Sydney", "value": "ap-southeast-2", "enabled": True}, {"name": "Singapore", "value": "ap-southeast-1", "enabled": False}, Separator(), "us-east-1", "us-west-1", + Separator(), ] @@ -50,6 +52,7 @@ def alternate(): regions = inquirer.checkbox( message="Select regions:", choices=question1_choice, + cycle=False, transformer=lambda result: "%s region%s selected" % (len(result), "s" if len(result) > 1 else ""), ).execute() diff --git a/examples/example_expand.py b/examples/example_expand.py index ab02408..15b18e7 100644 --- a/examples/example_expand.py +++ b/examples/example_expand.py @@ -1,4 +1,4 @@ -from InquirerPy import prompt, inquirer +from InquirerPy import inquirer, prompt from InquirerPy.separator import Separator @@ -31,6 +31,7 @@ def classic(): "choices": question1_choice, "message": "Pick your favourite:", "default": "o", + "cycle": False, }, { "type": "expand",