From 01788517f54f05623e4e00c593644f64ef2c0511 Mon Sep 17 00:00:00 2001 From: fullfox Date: Fri, 2 Feb 2024 17:27:20 +0100 Subject: [PATCH 1/5] Adding the search feature for List() --- examples/list_search.py | 20 ++++++++++++++ src/inquirer/questions.py | 4 +++ src/inquirer/render/console/_list.py | 15 +++++++++++ tests/integration/console_render/test_list.py | 27 +++++++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 examples/list_search.py diff --git a/examples/list_search.py b/examples/list_search.py new file mode 100644 index 00000000..39fdd514 --- /dev/null +++ b/examples/list_search.py @@ -0,0 +1,20 @@ +import os +import sys +from pprint import pprint + + +sys.path.append(os.path.realpath(".")) +import inquirer # noqa + +# To make the search case-insensitive +matcher = lambda entry, search: entry.lower().startswith(search.lower()) + +questions = [ + inquirer.List( + "size", message="What size do you need?", choices=["Jumbo", "Large", "Standard"], carousel=True, search=True, matcher=matcher + ), +] + +answers = inquirer.prompt(questions) + +pprint(answers) diff --git a/src/inquirer/questions.py b/src/inquirer/questions.py index 741d7780..8b8326f3 100644 --- a/src/inquirer/questions.py +++ b/src/inquirer/questions.py @@ -159,10 +159,14 @@ def __init__( carousel=False, other=False, autocomplete=None, + search=False, + matcher=None ): super().__init__(name, message, choices, default, ignore, validate, hints=hints, other=other) self.carousel = carousel self.autocomplete = autocomplete + self.search = search + self.matcher = matcher class Checkbox(Question): diff --git a/src/inquirer/render/console/_list.py b/src/inquirer/render/console/_list.py index 5cb48af3..3016a95d 100644 --- a/src/inquirer/render/console/_list.py +++ b/src/inquirer/render/console/_list.py @@ -11,12 +11,16 @@ class List(BaseConsoleRender): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.current = self._current_index() + self.input = "" @property def is_long(self): choices = self.question.choices or [] return len(choices) >= MAX_OPTIONS_DISPLAYED_AT_ONCE + def get_current_value(self): + return self.input if self.question.search else "" + def get_hint(self): try: choice = self.question.choices[self.current] @@ -90,6 +94,17 @@ def process_input(self, pressed): raise errors.EndOfInput(getattr(value, "value", value)) + if self.question.matcher != None: + if pressed.isprintable(): + self.input += pressed + for choice in self.question.choices: + if self.question.matcher(choice, self.input): + self.current = self.question.choices.index(choice) + break + + if pressed == chr(127): + self.input = self.input[:-1] + if pressed == key.CTRL_C: raise KeyboardInterrupt() diff --git a/tests/integration/console_render/test_list.py b/tests/integration/console_render/test_list.py index c4b2e747..4fa71be7 100644 --- a/tests/integration/console_render/test_list.py +++ b/tests/integration/console_render/test_list.py @@ -132,6 +132,33 @@ def test_ctrl_c_breaks_execution(self): with pytest.raises(KeyboardInterrupt): sut.render(question) + def test_type_char(self): + stdin = helper.event_factory('b', 'A', 'z', key.ENTER) + message = "Foo message" + variable = "Bar variable" + choices = ["foo", "bar", "bazz"] + matcher = lambda entry, search: entry.lower().startswith(search.lower()) + + question = questions.List(variable, message, choices=choices, carousel=True, matcher=matcher) + + sut = ConsoleRender(event_generator=stdin) + result = sut.render(question) + + assert result == "bazz" + + def test_type_char_without_matcher(self): + stdin = helper.event_factory('b', 'A', 'z', key.ENTER) + message = "Foo message" + variable = "Bar variable" + choices = ["foo", "bar", "bazz"] + + question = questions.List(variable, message, choices=choices, carousel=True) + + sut = ConsoleRender(event_generator=stdin) + result = sut.render(question) + + assert result == "foo" + def test_first_hint_is_shown(self): stdin = helper.event_factory(key.ENTER) message = "Foo message" From 709415142db5a546fea364f4328e3335d1ef96dd Mon Sep 17 00:00:00 2001 From: fullfox Date: Mon, 5 Feb 2024 10:00:59 +0100 Subject: [PATCH 2/5] Added Windows backspace support + flake8 compliant --- examples/list_search.py | 9 +++++++-- src/inquirer/render/console/_list.py | 7 ++++--- tests/integration/console_render/test_list.py | 5 ++++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/examples/list_search.py b/examples/list_search.py index 39fdd514..593b594f 100644 --- a/examples/list_search.py +++ b/examples/list_search.py @@ -6,12 +6,17 @@ sys.path.append(os.path.realpath(".")) import inquirer # noqa + # To make the search case-insensitive -matcher = lambda entry, search: entry.lower().startswith(search.lower()) +def matcher(entry, search): + return entry.lower().startswith(search.lower()) + questions = [ inquirer.List( - "size", message="What size do you need?", choices=["Jumbo", "Large", "Standard"], carousel=True, search=True, matcher=matcher + "size", message="What size do you need?", + choices=["Jumbo", "Large", "Standard"], + carousel=True, search=True, matcher=matcher ), ] diff --git a/src/inquirer/render/console/_list.py b/src/inquirer/render/console/_list.py index 3016a95d..c6674ff8 100644 --- a/src/inquirer/render/console/_list.py +++ b/src/inquirer/render/console/_list.py @@ -94,15 +94,16 @@ def process_input(self, pressed): raise errors.EndOfInput(getattr(value, "value", value)) - if self.question.matcher != None: + if self.question.matcher is not None: if pressed.isprintable(): self.input += pressed for choice in self.question.choices: if self.question.matcher(choice, self.input): self.current = self.question.choices.index(choice) break - - if pressed == chr(127): + + # BACKSPACE gives a \x7f on linux while it gives a \x08 on windows + if pressed == chr(127) or pressed == chr(8): self.input = self.input[:-1] if pressed == key.CTRL_C: diff --git a/tests/integration/console_render/test_list.py b/tests/integration/console_render/test_list.py index 1bdbeda4..73121820 100644 --- a/tests/integration/console_render/test_list.py +++ b/tests/integration/console_render/test_list.py @@ -137,7 +137,10 @@ def test_type_char(self): message = "Foo message" variable = "Bar variable" choices = ["foo", "bar", "bazz"] - matcher = lambda entry, search: entry.lower().startswith(search.lower()) + + # To make the search case-insensitive + def matcher(entry, search): + return entry.lower().startswith(search.lower()) question = questions.List(variable, message, choices=choices, carousel=True, matcher=matcher) From fef42dbc9d58eb31b29240db7616cc1711580b34 Mon Sep 17 00:00:00 2001 From: fullfox Date: Wed, 6 Mar 2024 15:18:42 +0100 Subject: [PATCH 3/5] new matcher signature --- examples/list_search.py | 11 +++++++-- src/inquirer/questions.py | 2 -- src/inquirer/render/console/_list.py | 18 ++++---------- tests/integration/console_render/test_list.py | 24 +++++++------------ 4 files changed, 22 insertions(+), 33 deletions(-) diff --git a/examples/list_search.py b/examples/list_search.py index 593b594f..be01f4bd 100644 --- a/examples/list_search.py +++ b/examples/list_search.py @@ -8,8 +8,15 @@ # To make the search case-insensitive -def matcher(entry, search): - return entry.lower().startswith(search.lower()) +def matcher(choices, pressedKey, searchString): + if pressedKey == key.BACKSPACE: + searchString = searchString[:-1] + elif pressedKey.isprintable(): + searchString += pressedKey + for i in range(len(choices)): + if choices[i].lower().startswith(searchString.lower()): + return (i, searchString) + return (0, searchString) questions = [ diff --git a/src/inquirer/questions.py b/src/inquirer/questions.py index ae5983c8..a93c8537 100644 --- a/src/inquirer/questions.py +++ b/src/inquirer/questions.py @@ -159,13 +159,11 @@ def __init__( carousel=False, other=False, autocomplete=None, - search=False, matcher=None ): super().__init__(name, message, choices, default, ignore, validate, hints=hints, other=other) self.carousel = carousel self.autocomplete = autocomplete - self.search = search self.matcher = matcher diff --git a/src/inquirer/render/console/_list.py b/src/inquirer/render/console/_list.py index c6674ff8..8a8afa31 100644 --- a/src/inquirer/render/console/_list.py +++ b/src/inquirer/render/console/_list.py @@ -11,16 +11,13 @@ class List(BaseConsoleRender): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.current = self._current_index() - self.input = "" + self.search = "" @property def is_long(self): choices = self.question.choices or [] return len(choices) >= MAX_OPTIONS_DISPLAYED_AT_ONCE - def get_current_value(self): - return self.input if self.question.search else "" - def get_hint(self): try: choice = self.question.choices[self.current] @@ -95,16 +92,9 @@ def process_input(self, pressed): raise errors.EndOfInput(getattr(value, "value", value)) if self.question.matcher is not None: - if pressed.isprintable(): - self.input += pressed - for choice in self.question.choices: - if self.question.matcher(choice, self.input): - self.current = self.question.choices.index(choice) - break - - # BACKSPACE gives a \x7f on linux while it gives a \x08 on windows - if pressed == chr(127) or pressed == chr(8): - self.input = self.input[:-1] + (index, search) = self.question.matcher(self.question.choices, pressed, self.search) + self.search = search + self.current = index if pressed == key.CTRL_C: raise KeyboardInterrupt() diff --git a/tests/integration/console_render/test_list.py b/tests/integration/console_render/test_list.py index 73121820..7f1ea692 100644 --- a/tests/integration/console_render/test_list.py +++ b/tests/integration/console_render/test_list.py @@ -139,8 +139,15 @@ def test_type_char(self): choices = ["foo", "bar", "bazz"] # To make the search case-insensitive - def matcher(entry, search): - return entry.lower().startswith(search.lower()) + def matcher(choices, pressedKey, searchString): + if pressedKey == key.BACKSPACE: + searchString = searchString[:-1] + elif pressedKey.isprintable(): + searchString += pressedKey + for i in range(len(choices)): + if choices[i].lower().startswith(searchString.lower()): + return (i, searchString) + return (0, searchString) question = questions.List(variable, message, choices=choices, carousel=True, matcher=matcher) @@ -149,19 +156,6 @@ def matcher(entry, search): assert result == "bazz" - def test_type_char_without_matcher(self): - stdin = helper.event_factory('b', 'A', 'z', key.ENTER) - message = "Foo message" - variable = "Bar variable" - choices = ["foo", "bar", "bazz"] - - question = questions.List(variable, message, choices=choices, carousel=True) - - sut = ConsoleRender(event_generator=stdin) - result = sut.render(question) - - assert result == "foo" - def test_first_hint_is_shown(self): stdin = helper.event_factory(key.ENTER) message = "Foo message" From 10e660dd40c0846fd6ce7b8eee30c009f9b65a5e Mon Sep 17 00:00:00 2001 From: fullfox Date: Wed, 6 Mar 2024 15:23:55 +0100 Subject: [PATCH 4/5] tiny fix --- examples/list_search.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/list_search.py b/examples/list_search.py index be01f4bd..d88c24b2 100644 --- a/examples/list_search.py +++ b/examples/list_search.py @@ -5,6 +5,7 @@ sys.path.append(os.path.realpath(".")) import inquirer # noqa +from readchar import key # To make the search case-insensitive @@ -23,7 +24,7 @@ def matcher(choices, pressedKey, searchString): inquirer.List( "size", message="What size do you need?", choices=["Jumbo", "Large", "Standard"], - carousel=True, search=True, matcher=matcher + carousel=True, matcher=matcher ), ] From 3e6a4f730503ae8eb35420bf4f5cb739a50afc8e Mon Sep 17 00:00:00 2001 From: fullfox Date: Wed, 6 Mar 2024 15:25:12 +0100 Subject: [PATCH 5/5] print search --- src/inquirer/render/console/_list.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/inquirer/render/console/_list.py b/src/inquirer/render/console/_list.py index 8a8afa31..c1ae947c 100644 --- a/src/inquirer/render/console/_list.py +++ b/src/inquirer/render/console/_list.py @@ -18,6 +18,9 @@ def is_long(self): choices = self.question.choices or [] return len(choices) >= MAX_OPTIONS_DISPLAYED_AT_ONCE + def get_current_value(self): + return self.search + def get_hint(self): try: choice = self.question.choices[self.current]