From 28c449a2a8f5d2e49442698611af14d5def2b742 Mon Sep 17 00:00:00 2001 From: Tim Harris Date: Mon, 26 Oct 2020 16:56:02 -0400 Subject: [PATCH 1/6] Adding list prompt that filters when you type --- .gitignore | 4 +- PyInquirer/prompt.py | 4 +- PyInquirer/prompts/listwithfilter.py | 316 +++++++++++++++++++++++++++ README.rst | 11 + examples/listwithfilter.py | 55 +++++ tests/test_example_listwithfilter.py | 42 ++++ 6 files changed, 429 insertions(+), 3 deletions(-) create mode 100644 PyInquirer/prompts/listwithfilter.py create mode 100644 examples/listwithfilter.py create mode 100644 tests/test_example_listwithfilter.py diff --git a/.gitignore b/.gitignore index 9577fd5..315c344 100644 --- a/.gitignore +++ b/.gitignore @@ -73,4 +73,6 @@ nosetests.xml .vscode/ -localtests/ \ No newline at end of file +localtests/ + +.venv \ No newline at end of file diff --git a/PyInquirer/prompt.py b/PyInquirer/prompt.py index e4f3811..5ecbc07 100644 --- a/PyInquirer/prompt.py +++ b/PyInquirer/prompt.py @@ -2,7 +2,7 @@ from contextlib import contextmanager from . import PromptParameterException, prompts -from .prompts import list, confirm, input, password, checkbox, rawlist, expand, editor +from .prompts import list, listwithfilter, confirm, input, password, checkbox, rawlist, expand, editor from prompt_toolkit.patch_stdout import patch_stdout as pt_patch_stdout from prompt_toolkit.shortcuts import PromptSession from prompt_toolkit.application import Application @@ -68,7 +68,7 @@ def prompt(questions, answers=None, **kwargs): _kwargs['default'] = question['default'](answers) with pt_patch_stdout() if patch_stdout else _dummy_context_manager(): - result = getattr(prompts, type_).question(message, **_kwargs) + result = getattr(prompts, type_).question(message, **_kwargs) if isinstance(result, PromptSession): diff --git a/PyInquirer/prompts/listwithfilter.py b/PyInquirer/prompts/listwithfilter.py new file mode 100644 index 0000000..826f654 --- /dev/null +++ b/PyInquirer/prompts/listwithfilter.py @@ -0,0 +1,316 @@ +# -*- coding: utf-8 -*- +""" +`list` type question + +Complete re-write with filter support. Taken from: +https://github.com/gbataille/password-organizer/blob/master/password_organizer/cli_menu/prompts/listmenu.py +""" +from dataclasses import dataclass +from prompt_toolkit.application import Application +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.keys import Keys +from prompt_toolkit.filters import Condition, IsDone +from prompt_toolkit.layout import Layout +from prompt_toolkit.layout.controls import ( + FormattedTextControl, GetLinePrefixCallable, UIContent, UIControl +) +from prompt_toolkit.layout.containers import ConditionalContainer, HSplit, Window +from prompt_toolkit.layout.dimension import LayoutDimension as D +import string +from typing import Generic, List, Optional, TypeVar + +from .common import default_style + + + +class ChoicesControl(UIControl): + """ + Menu to display some textual choices. + Provide a search feature by just typing the start of the entry desired + """ + def __init__(self, choices, **kwargs): + # Selection to keep consistent + self._selected_choice = None + self._selected_index: int = -1 + + self._answered = False + self._search_string: Optional[str] = None + self._choices = choices + self._cached_choices = None + + self._init_choices(default=kwargs.pop('default')) + super().__init__(**kwargs) + + def _init_choices(self, default=None): + if default is not None and default not in self._choices: + raise ValueError(f"Default value {default} is not part of the given choices") + self._compute_available_choices(default=default) + + @property + def is_answered(self) -> bool: + return self._answered + + @is_answered.setter + def is_answered(self, value: bool) -> None: + self._answered = value + + def _get_available_choices(self) -> None: + if self._cached_choices is None: + self._compute_available_choices() + + return self._cached_choices or [] + + + def _compute_available_choices(self, default = None) -> None: + self._cached_choices = [] + + for choice in self._choices: + if self._search_string: + if isinstance(choice, str): + if self._search_string in choice.lower(): + self._cached_choices.append(choice) + else: + if self._search_string in choice.get('name').lower(): + self._cached_choices.append(choice) + else: + self._cached_choices.append(choice) + + if self._cached_choices == []: + self._selected_choice = None + self._selected_index = -1 + else: + if default is not None: + self._selected_choice = default + self._selected_index = self._cached_choices.index(default) + + if self._selected_choice not in self._cached_choices: + self._selected_choice = self._cached_choices[0] + self._selected_index = 0 + if isinstance(self._selected_choice, str): + pass + else: + while self._selected_choice.get('disabled', False): + self.select_next_choice() + + def _reset_cached_choices(self) -> None: + self._cached_choices = None + + def get_selection(self): + return self._selected_choice + + def select_next_choice(self) -> None: + if not self._cached_choices or self._selected_choice is None: + return + + def _next(): + self._selected_index += 1 + self._selected_choice = self._cached_choices[self._selected_index % self.choice_count] + + _next() + if isinstance(self._selected_choice, str): + pass + else: + while self._selected_choice.get('disabled', False): + _next() + + def select_previous_choice(self) -> None: + if not self._cached_choices or self._selected_choice is None: + return + + def _prev(): + self._selected_index -= 1 + self._selected_choice = self._cached_choices[self._selected_index % self.choice_count] + + _prev() + if isinstance(self._selected_choice, str): + pass + else: + while self._selected_choice.get('disabled', False): + _prev() + + def preferred_width(self, max_available_width: int) -> int: + max_elem_width = max(list(map(lambda x: x.display_length, self._choices))) + return min(max_elem_width, max_available_width) + + def preferred_height( + self, + width: int, + max_available_height: int, + wrap_lines: bool, + get_line_prefix: Optional[GetLinePrefixCallable], + ) -> Optional[int]: + return self.choice_count + + def create_content(self, width: int, height: int) -> UIContent: + + def _get_line_tokens(line_number): + choice = self._get_available_choices()[line_number] + tokens = [] + + selected = (choice == self.get_selection()) + tokens.append(('class:pointer' if selected else '', ' \u276f ' if selected + else ' ')) + if selected: + tokens.append(('[SetCursorPosition]', '')) + + if isinstance(choice, str): + tokens.append(('class:selected' if selected else '', str(choice))) + else: + if choice.get('disabled', False): + token_text = choice.get('name', choice) + if choice.get('disabled_reason', False): + token_text += f' ({choice.get("disabled_reason")})' + tokens.append(('class:selected' if selected else 'class:disabled', token_text)) + else: + tokens.append(('class:selected' if selected else '', str(choice.get('name', choice)))) + + return tokens + + return UIContent( + get_line=_get_line_tokens, + line_count=self.choice_count, + ) + + @property + def choice_count(self): + return len(self._get_available_choices()) + + def get_search_string_tokens(self): + if self._search_string is None: + return None + + return [ + ('', '\n'), + ('class:question-mark', '/ '), + ('class:search', self._search_string), + ('class:question-mark', '...'), + ] + + def append_to_search_string(self, char: str) -> None: + """ Appends a character to the search string """ + if self._search_string is None: + self._search_string = '' + self._search_string += char + self._reset_cached_choices() + + def remove_last_char_from_search_string(self) -> None: + """ Remove the last character from the search string (~backspace) """ + if self._search_string and len(self._search_string) > 1: + self._search_string = self._search_string[:-1] + else: + self._search_string = None + self._reset_cached_choices() + + def reset_search_string(self) -> None: + self._search_string = None + + +def question(message, **kwargs): + """ + Builds a `prompt-toolkit` Application that display a list of choices (ChoiceControl) along with + search features and key bindings + + Paramaters + ========== + kwargs: Dict[Any, Any] + Any additional arguments that a prompt_toolkit.application.Application can take. Passed + as-is + """ + # TODO disabled, dict choices + if not 'choices' in kwargs: + raise PromptParameterException('choices') + + choices = kwargs.pop('choices', None) + default = kwargs.pop('default', None) + key_bindings = kwargs.pop('key_bindings', None) + qmark = kwargs.pop('qmark', '?') + # TODO style defaults on detail level + style = kwargs.pop('style', default_style) + + if key_bindings is None: + key_bindings = KeyBindings() + + choices_control = ChoicesControl(choices, default=default) + + def get_prompt_tokens(): + tokens = [] + + tokens.append(('class:question-mark', qmark)) + tokens.append(('class:question', ' %s ' % message)) + if choices_control.is_answered: + if isinstance(choices_control.get_selection(), str): + tokens.append(('class:answer', ' ' + choices_control.get_selection())) + else: + tokens.append(('class:answer', ' ' + choices_control.get_selection().get('name'))) + else: + tokens.append(('class:instruction', ' (Use arrow keys)')) + return tokens + + @Condition + def has_search_string(): + return choices_control.get_search_string_tokens is not None + + @key_bindings.add(Keys.ControlQ, eager=True) + def exit_menu(event): + event.app.exit(exception=KeyboardInterrupt()) + + if not key_bindings.get_bindings_for_keys((Keys.ControlC,)): + key_bindings.add(Keys.ControlC, eager=True)(exit_menu) + + @key_bindings.add(Keys.Down, eager=True) + def move_cursor_down(_event): # pylint:disable=unused-variable + choices_control.select_next_choice() + + @key_bindings.add(Keys.Up, eager=True) + def move_cursor_up(_event): # pylint:disable=unused-variable + choices_control.select_previous_choice() + + @key_bindings.add(Keys.Enter, eager=True) + def set_answer(event): # pylint:disable=unused-variable + choices_control.is_answered = True + choices_control.reset_search_string() + if isinstance(choices_control.get_selection(), str): + event.app.exit(result=choices_control.get_selection()) + else: + event.app.exit(result=choices_control.get_selection().get('value')) + + def search_filter(event): + choices_control.append_to_search_string(event.key_sequence[0].key) + + for character in string.printable: + key_bindings.add(character, eager=True)(search_filter) + + @key_bindings.add(Keys.Backspace, eager=True) + def delete_from_search_filter(_event): # pylint:disable=unused-variable + choices_control.remove_last_char_from_search_string() + + layout = Layout( + HSplit([ + # Question + Window( + height=D.exact(1), + content=FormattedTextControl(get_prompt_tokens), + always_hide_cursor=True, + ), + # Choices + ConditionalContainer( + Window(choices_control), + filter=~IsDone() # pylint:disable=invalid-unary-operand-type + ), + # Searched string + ConditionalContainer( + Window( + height=D.exact(2), + content=FormattedTextControl(choices_control.get_search_string_tokens) + ), + filter=has_search_string & ~IsDone() # pylint:disable=invalid-unary-operand-type + ), + ]) + ) + + return Application( + layout=layout, + key_bindings=key_bindings, + mouse_support=False, + style=style + ) \ No newline at end of file diff --git a/README.rst b/README.rst index 50dc6ea..6c35c86 100644 --- a/README.rst +++ b/README.rst @@ -151,6 +151,17 @@ in the array or a choice ``value``) |List prompt| s --- +List with Filter - ``{type: 'listwithfilter'}`` +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Same as list but filters choices as you type. + +Takes ``type``, ``name``, ``message``, ``choices``\ [, ``default``, +``filter``] properties. (Note that default must be the choice ``index`` +in the array or a choice ``value``) + +|List prompt| s --- + Raw List - ``{type: 'rawlist'}`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/examples/listwithfilter.py b/examples/listwithfilter.py new file mode 100644 index 0000000..696c4c1 --- /dev/null +++ b/examples/listwithfilter.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +""" +list prompt example +""" +from __future__ import print_function, unicode_literals + +from pprint import pprint + +from PyInquirer import prompt, Separator + +from examples import custom_style_2 + + + +def get_delivery_options(answers): + options = ['bike', 'car', 'truck'] + if answers['size'] == 'jumbo': + options.append('helicopter') + return options + + +questions = [ + { + 'type': 'list', + 'name': 'theme', + 'message': 'What do you want to do?', + 'choices': [ + 'Order a pizza', + 'Make a reservation', + Separator(), + 'Ask for opening hours', + { + 'name': 'Contact support', + 'disabled': 'Unavailable at this time' + }, + 'Talk to the receptionist' + ] + }, + { + 'type': 'list', + 'name': 'size', + 'message': 'What size do you need?', + 'choices': ['Jumbo', 'Large', 'Standard', 'Medium', 'Small', 'Micro'], + 'filter': lambda val: val.lower() + }, + { + 'type': 'list', + 'name': 'delivery', + 'message': 'Which vehicle you want to use for delivery?', + 'choices': get_delivery_options, + }, +] + +answers = prompt.prompt(questions, style=custom_style_2) +pprint(answers) diff --git a/tests/test_example_listwithfilter.py b/tests/test_example_listwithfilter.py new file mode 100644 index 0000000..10aa1b9 --- /dev/null +++ b/tests/test_example_listwithfilter.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +import textwrap + +from .helpers import keys +from .helpers import create_example_fixture + + +example_app = create_example_fixture('examples/listwithfilter.py') + + +def test_list(example_app): + example_app.expect(textwrap.dedent("""\ + ? What do you want to do? (Use arrow keys) + ❯ Order a pizza + Make a reservation + --------------- + Ask for opening hours + - Contact support (Unavailable at this time) + Talk to the receptionist""")) + example_app.write(keys.ENTER) + example_app.expect(textwrap.dedent("""\ + ? What do you want to do? Order a pizza + ? What size do you need? (Use arrow keys) + ❯ Jumbo + Large + Standard + Medium + Small + Micro""")) + example_app.write(keys.ENTER) + example_app.expect(textwrap.dedent("""\ + ? What size do you need? Jumbo + ? Which vehicle you want to use for delivery? (Use arrow keys) + ❯ bike + car + truck + helicopter""")) + example_app.write(keys.ENTER) + example_app.expect(textwrap.dedent("""\ + ? Which vehicle you want to use for delivery? bike + {'delivery': 'bike', 'size': 'jumbo', 'theme': 'Order a pizza'} + """)) From b2c8d34bb114d8760326e58d4adb11b1853e333d Mon Sep 17 00:00:00 2001 From: Tim Harris Date: Mon, 26 Oct 2020 18:00:36 -0400 Subject: [PATCH 2/6] Updating to support separators --- PyInquirer/prompts/listwithfilter.py | 45 +++++++++++++++++----------- examples/listwithfilter.py | 6 ++-- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/PyInquirer/prompts/listwithfilter.py b/PyInquirer/prompts/listwithfilter.py index 826f654..2632a1c 100644 --- a/PyInquirer/prompts/listwithfilter.py +++ b/PyInquirer/prompts/listwithfilter.py @@ -18,7 +18,7 @@ from prompt_toolkit.layout.dimension import LayoutDimension as D import string from typing import Generic, List, Optional, TypeVar - +from ..separator import Separator from .common import default_style @@ -66,7 +66,9 @@ def _compute_available_choices(self, default = None) -> None: for choice in self._choices: if self._search_string: - if isinstance(choice, str): + if isinstance(choice, Separator): + self._cached_choices.append(choice) + elif isinstance(choice, str): if self._search_string in choice.lower(): self._cached_choices.append(choice) else: @@ -88,6 +90,8 @@ def _compute_available_choices(self, default = None) -> None: self._selected_index = 0 if isinstance(self._selected_choice, str): pass + elif isinstance(self._selected_choice, Separator): + self.select_next_choice() else: while self._selected_choice.get('disabled', False): self.select_next_choice() @@ -109,9 +113,12 @@ def _next(): _next() if isinstance(self._selected_choice, str): pass + elif isinstance(self._selected_choice, Separator): + _next() else: - while self._selected_choice.get('disabled', False): + while isinstance(self._selected_choice, dict) and self._selected_choice.get('disabled', False): _next() + def select_previous_choice(self) -> None: if not self._cached_choices or self._selected_choice is None: @@ -124,8 +131,10 @@ def _prev(): _prev() if isinstance(self._selected_choice, str): pass + elif isinstance(self._selected_choice, Separator): + _prev() else: - while self._selected_choice.get('disabled', False): + while isinstance(self._selected_choice, dict) and self._selected_choice.get('disabled', False): _prev() def preferred_width(self, max_available_width: int) -> int: @@ -148,21 +157,23 @@ def _get_line_tokens(line_number): tokens = [] selected = (choice == self.get_selection()) - tokens.append(('class:pointer' if selected else '', ' \u276f ' if selected - else ' ')) - if selected: - tokens.append(('[SetCursorPosition]', '')) - - if isinstance(choice, str): - tokens.append(('class:selected' if selected else '', str(choice))) + if isinstance(choice, Separator): + tokens.append(('class:separator', ' %s\n' % choice)) else: - if choice.get('disabled', False): - token_text = choice.get('name', choice) - if choice.get('disabled_reason', False): - token_text += f' ({choice.get("disabled_reason")})' - tokens.append(('class:selected' if selected else 'class:disabled', token_text)) + tokens.append(('class:pointer' if selected else '', ' \u276f ' if selected + else ' ')) + if selected: + tokens.append(('[SetCursorPosition]', '')) + if isinstance(choice, str): + tokens.append(('class:selected' if selected else '', str(choice))) else: - tokens.append(('class:selected' if selected else '', str(choice.get('name', choice)))) + if choice.get('disabled', False): + token_text = choice.get('name') + if isinstance(choice.get('disabled'), str): + token_text += f' ({choice.get("disabled")})' + tokens.append(('class:selected' if selected else 'class:disabled', token_text)) + else: + tokens.append(('class:selected' if selected else '', str(choice.get('name', choice)))) return tokens diff --git a/examples/listwithfilter.py b/examples/listwithfilter.py index 696c4c1..e2af75c 100644 --- a/examples/listwithfilter.py +++ b/examples/listwithfilter.py @@ -21,7 +21,7 @@ def get_delivery_options(answers): questions = [ { - 'type': 'list', + 'type': 'listwithfilter', 'name': 'theme', 'message': 'What do you want to do?', 'choices': [ @@ -37,14 +37,14 @@ def get_delivery_options(answers): ] }, { - 'type': 'list', + 'type': 'listwithfilter', 'name': 'size', 'message': 'What size do you need?', 'choices': ['Jumbo', 'Large', 'Standard', 'Medium', 'Small', 'Micro'], 'filter': lambda val: val.lower() }, { - 'type': 'list', + 'type': 'listwithfilter', 'name': 'delivery', 'message': 'Which vehicle you want to use for delivery?', 'choices': get_delivery_options, From 9b808fbc76672b735bdebec7eb8b0fcde9a9936b Mon Sep 17 00:00:00 2001 From: Tim Harris Date: Tue, 27 Oct 2020 12:26:22 -0400 Subject: [PATCH 3/6] updating search to lower() --- PyInquirer/prompts/listwithfilter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PyInquirer/prompts/listwithfilter.py b/PyInquirer/prompts/listwithfilter.py index 2632a1c..144531f 100644 --- a/PyInquirer/prompts/listwithfilter.py +++ b/PyInquirer/prompts/listwithfilter.py @@ -69,10 +69,10 @@ def _compute_available_choices(self, default = None) -> None: if isinstance(choice, Separator): self._cached_choices.append(choice) elif isinstance(choice, str): - if self._search_string in choice.lower(): + if self._search_string.lower() in choice.lower(): self._cached_choices.append(choice) else: - if self._search_string in choice.get('name').lower(): + if self._search_string.lower() in choice.get('name').lower(): self._cached_choices.append(choice) else: self._cached_choices.append(choice) From b5b20babc6e0baf37742265aa94a83961d527bc5 Mon Sep 17 00:00:00 2001 From: Tim Harris Date: Wed, 28 Oct 2020 09:25:28 -0400 Subject: [PATCH 4/6] Fixed lower() issue --- PyInquirer/prompts/listwithfilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PyInquirer/prompts/listwithfilter.py b/PyInquirer/prompts/listwithfilter.py index 144531f..074389a 100644 --- a/PyInquirer/prompts/listwithfilter.py +++ b/PyInquirer/prompts/listwithfilter.py @@ -71,7 +71,7 @@ def _compute_available_choices(self, default = None) -> None: elif isinstance(choice, str): if self._search_string.lower() in choice.lower(): self._cached_choices.append(choice) - else: + else: if self._search_string.lower() in choice.get('name').lower(): self._cached_choices.append(choice) else: From 7267a23642bc56d29e87ddddcc6fca6bd5cb4b09 Mon Sep 17 00:00:00 2001 From: Tim Harris Date: Mon, 2 Nov 2020 16:32:03 -0500 Subject: [PATCH 5/6] Update listwithfilter.py --- PyInquirer/prompts/listwithfilter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PyInquirer/prompts/listwithfilter.py b/PyInquirer/prompts/listwithfilter.py index 074389a..c3414db 100644 --- a/PyInquirer/prompts/listwithfilter.py +++ b/PyInquirer/prompts/listwithfilter.py @@ -254,7 +254,7 @@ def get_prompt_tokens(): else: tokens.append(('class:answer', ' ' + choices_control.get_selection().get('name'))) else: - tokens.append(('class:instruction', ' (Use arrow keys)')) + tokens.append(('class:instruction', ' (Use arrow keys or type to filter)')) return tokens @Condition @@ -324,4 +324,4 @@ def delete_from_search_filter(_event): # pylint:disable=unused-variable key_bindings=key_bindings, mouse_support=False, style=style - ) \ No newline at end of file + ) From ea0232feb312335e514db97450c7054960603159 Mon Sep 17 00:00:00 2001 From: JJ Miller Date: Wed, 17 May 2023 11:12:52 -0400 Subject: [PATCH 6/6] Fix a bug in `listwithfilter` where specifying a default value when using dictionary choices would cause an exception. --- PyInquirer/prompts/listwithfilter.py | 19 +++++++++++++++---- examples/listwithfilter.py | 10 ++++++++++ tests/test_example_listwithfilter.py | 7 +++++++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/PyInquirer/prompts/listwithfilter.py b/PyInquirer/prompts/listwithfilter.py index c3414db..f5ad98f 100644 --- a/PyInquirer/prompts/listwithfilter.py +++ b/PyInquirer/prompts/listwithfilter.py @@ -42,7 +42,7 @@ def __init__(self, choices, **kwargs): super().__init__(**kwargs) def _init_choices(self, default=None): - if default is not None and default not in self._choices: + if default is not None and self._find_choice(default) is None: raise ValueError(f"Default value {default} is not part of the given choices") self._compute_available_choices(default=default) @@ -82,8 +82,8 @@ def _compute_available_choices(self, default = None) -> None: self._selected_index = -1 else: if default is not None: - self._selected_choice = default - self._selected_index = self._cached_choices.index(default) + self._selected_choice = self._find_choice(default) + self._selected_index = self._cached_choices.index(self._selected_choice) if self._selected_choice not in self._cached_choices: self._selected_choice = self._cached_choices[0] @@ -96,6 +96,17 @@ def _compute_available_choices(self, default = None) -> None: while self._selected_choice.get('disabled', False): self.select_next_choice() + def _find_choice(self, value): + for choice in self._choices: + if isinstance(choice, Separator): + continue + elif isinstance(choice, str): + if value == choice: + return choice + else: + if value == choice.get('value'): + return choice + def _reset_cached_choices(self) -> None: self._cached_choices = None @@ -118,7 +129,7 @@ def _next(): else: while isinstance(self._selected_choice, dict) and self._selected_choice.get('disabled', False): _next() - + def select_previous_choice(self) -> None: if not self._cached_choices or self._selected_choice is None: diff --git a/examples/listwithfilter.py b/examples/listwithfilter.py index e2af75c..23b5960 100644 --- a/examples/listwithfilter.py +++ b/examples/listwithfilter.py @@ -43,6 +43,16 @@ def get_delivery_options(answers): 'choices': ['Jumbo', 'Large', 'Standard', 'Medium', 'Small', 'Micro'], 'filter': lambda val: val.lower() }, + { + 'type': 'listwithfilter', + 'name': 'cook_level', + 'message': 'How well done do you want it?', + 'choices': [ + {'name': 'Crispy', 'value': 'crispy'}, + {'name': 'Normal', 'value': 'normal'}, + {'name': 'Soft', 'value': 'soft'}], + 'default': 'crispy' + }, { 'type': 'listwithfilter', 'name': 'delivery', diff --git a/tests/test_example_listwithfilter.py b/tests/test_example_listwithfilter.py index 10aa1b9..da8aa76 100644 --- a/tests/test_example_listwithfilter.py +++ b/tests/test_example_listwithfilter.py @@ -30,6 +30,13 @@ def test_list(example_app): example_app.write(keys.ENTER) example_app.expect(textwrap.dedent("""\ ? What size do you need? Jumbo + ? How well done do you want it? (Use arrow keys) + Crispy + ❯ Normal + Soft""")) + example_app.write(keys.ENTER) + example_app.expect(textwrap.dedent("""\ + ? How well done do you want it? Normal ? Which vehicle you want to use for delivery? (Use arrow keys) ❯ bike car