Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
44366b4
add searchable filter list
badranX Feb 13, 2025
2c4b131
add example for filter_list
badranX Feb 13, 2025
1ab7415
add callback for filter_list choice change
badranX Feb 13, 2025
ab3794e
add filter_list callback example
badranX Feb 13, 2025
a094117
remove filter features from Question class
badranX Mar 13, 2025
5742367
remove list cast from filter_func
badranX Mar 13, 2025
70a9e39
refactor lambda to method in filter_list
badranX Mar 13, 2025
8319b5f
remove redundant function calls
badranX Mar 13, 2025
5311606
add filter_list not found behaviour
badranX Mar 13, 2025
8819962
simplify filter_list example
badranX Mar 13, 2025
45391df
add filter_list to * import
badranX Mar 13, 2025
b6e04bc
fix filter_list elements clearing
badranX Mar 14, 2025
c7f776f
add argv to filter_list
badranX Mar 17, 2025
6cc851c
add filter_list tests
badranX Mar 17, 2025
8d278f6
fix filter_list tag test
badranX Mar 18, 2025
6f868fb
remove autocomplete behaviour
badranX Mar 18, 2025
ba87986
remove FilterList cursor & mandatory filter_func
badranX Mar 19, 2025
2696be9
add more FilterList example tests
badranX Mar 19, 2025
74df06c
refactor _filter_list
badranX Mar 20, 2025
b49d3d9
refactor FilterList acceptance tests
badranX Mar 20, 2025
1dffc0a
add default filter_func
badranX Mar 21, 2025
0348143
add render test for filter_list
badranX Mar 21, 2025
f69c173
adjust not_found behaviour in filter_list
badranX Mar 22, 2025
c36daa9
add more tests for FilterList
badranX Mar 22, 2025
bb094b2
simplify filter_list: remove choice callback
badranX Mar 22, 2025
98c4ad3
fix default FilterList searches hints
badranX Mar 23, 2025
2818ec5
fix FilterList render test bug
badranX Mar 23, 2025
7eded9a
refactor FilterList
badranX Mar 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions examples/filter_list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import sys
from pprint import pprint

import inquirer # noqa


choices_map = str.__dict__
choices = sorted(choices_map.keys())

# prepare FilterList arguments
args = sys.argv[1:]
if "hint" in args:
choices_hints = {k: f"{str(v)[:10]}..." for k, v in choices_map.items()}
else:
choices_hints = None
carousel = True if "carousel" in args else False
other = True if "other" in args else False
choices = [(k, str(choices_map[k])[:5]) for k in choices] if "tag" in args else choices


def filter_func(text, all_choices):
# `all_choices` is the global `choices` in this example
# in `tag` choices, tuples are cast to `str`. It's the user's responsibility to change this behaviour
return filter(lambda x: text in str(x), all_choices)


questions = [
inquirer.FilterList(
"attribute",
message="Select item ",
choices=choices,
carousel=carousel,
other=other,
hints=choices_hints,
filter_func=filter_func,
),
]

answers = inquirer.prompt(questions)

pprint(answers)
2 changes: 2 additions & 0 deletions src/inquirer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from inquirer.questions import Confirm
from inquirer.questions import Editor
from inquirer.questions import List
from inquirer.questions import FilterList
from inquirer.questions import Password
from inquirer.questions import Path
from inquirer.questions import Text
Expand All @@ -25,6 +26,7 @@
"Password",
"Confirm",
"List",
"FilterList",
"Checkbox",
"Path",
"load_from_list",
Expand Down
35 changes: 35 additions & 0 deletions src/inquirer/questions.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,41 @@ def __init__(
self.autocomplete = autocomplete


class FilterList(Question):
kind = "filter_list"

def __init__(
self,
name,
message="",
choices=None,
hints=None,
default=None,
ignore=False,
validate=True,
carousel=False,
other=False,
autocomplete=None,
filter_func=None,
):
super().__init__(name, message, choices, default, ignore, validate, hints=hints, other=other)
self.carousel = carousel
self.autocomplete = autocomplete
self.filter_func = filter_func if filter_func else self._filter_func
self._all_choices = choices

def _filter_func(self, text, all_choices):
# reset 'self.choices' property to use str() cast, filter what user sees
self._choices = all_choices
return filter(lambda x: text in str(x), self.choices)

def apply_filter(self, filter_func):
self._choices = list(filter_func(self._all_choices))

def remove_filter(self):
self._choices = self._all_choices


class Checkbox(Question):
kind = "checkbox"

Expand Down
2 changes: 2 additions & 0 deletions src/inquirer/render/console/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from inquirer.render.console._confirm import Confirm
from inquirer.render.console._editor import Editor
from inquirer.render.console._list import List
from inquirer.render.console._filter_list import FilterList
from inquirer.render.console._password import Password
from inquirer.render.console._path import Path
from inquirer.render.console._text import Text
Expand Down Expand Up @@ -158,6 +159,7 @@ def render_factory(self, question_type):
"password": Password,
"confirm": Confirm,
"list": List,
"filter_list": FilterList,
"checkbox": Checkbox,
"path": Path,
}
Expand Down
149 changes: 149 additions & 0 deletions src/inquirer/render/console/_filter_list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import sys

from readchar import key

from inquirer import errors
from inquirer.render.console._other import GLOBAL_OTHER_CHOICE
from inquirer.render.console.base import MAX_OPTIONS_DISPLAYED_AT_ONCE
from inquirer.render.console.base import BaseConsoleRender
from inquirer.render.console.base import half_options


class FilterList(BaseConsoleRender):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.current = self._current_index()
self.current_text = ""

def get_current_value(self):
return self.current_text

@property
def is_long(self):
choices = self.question.choices or []
return len(choices) >= MAX_OPTIONS_DISPLAYED_AT_ONCE

def get_hint(self):
try:
choice = self.question.choices[self.current]
hint = self.question.hints[choice]
if hint:
return f"{choice}: {hint}"
else:
return f"{choice}"
except (KeyError, IndexError):
return ""

def get_options(self):
choices = self.question.choices or []
if self.is_long:
cmin = 0
cmax = MAX_OPTIONS_DISPLAYED_AT_ONCE

if half_options < self.current < len(choices) - half_options:
cmin += self.current - half_options
cmax += self.current - half_options
elif self.current >= len(choices) - half_options:
cmin += len(choices) - MAX_OPTIONS_DISPLAYED_AT_ONCE
cmax += len(choices)

cchoices = choices[cmin:cmax]
else:
cchoices = choices

ending_milestone = max(len(choices) - half_options, half_options + 1)
is_in_beginning = self.current <= half_options
is_in_middle = half_options < self.current < ending_milestone
is_in_end = self.current >= ending_milestone

for index, choice in enumerate(cchoices):
end_index = ending_milestone + index - half_options - 1
if (
(is_in_middle and index == half_options)
or (is_in_beginning and index == self.current)
or (is_in_end and end_index == self.current)
):
color = self.theme.List.selection_color
symbol = "+" if choice == GLOBAL_OTHER_CHOICE else self.theme.List.selection_cursor
else:
color = self.theme.List.unselected_color
symbol = " " if choice == GLOBAL_OTHER_CHOICE else " " * len(self.theme.List.selection_cursor)
yield choice, symbol, color
self._clear_eos_and_flush()

def _clear_eos_and_flush(self):
print(self.terminal.clear_eos(), end="")
sys.stdout.flush()

def _get_current_choice(self):
try:
return self.question.choices[self.current]
except (KeyError, IndexError):
return None

def process_input(self, pressed):
self._process_control_input(pressed)
self._process_text_input(pressed)

def _process_control_input(self, pressed):
question = self.question
if pressed == key.UP:
if question.carousel and self.current == 0:
self.current = len(question.choices) - 1
else:
self.current = max(0, self.current - 1)
return
if pressed == key.DOWN or pressed == key.TAB:
if question.carousel and self.current == len(question.choices) - 1:
self.current = 0
else:
self.current = min(len(self.question.choices) - 1, self.current + 1)
return
if pressed == key.ENTER:
value = self._get_current_choice()
if not value:
# filter_list can be empty, then key.ENTER returns user search input
value = self.get_current_value()

if value == GLOBAL_OTHER_CHOICE:
value = self.other_input()
if not value:
# Clear the print inquirer.text made, since the user didn't enter anything
print(self.terminal.move_up + self.terminal.clear_eol, end="")
return

raise errors.EndOfInput(getattr(value, "value", value))

if pressed == key.CTRL_C:
raise KeyboardInterrupt()

def _process_text_input(self, pressed):
prev_text = self.current_text

if pressed == key.ENTER or pressed == key.TAB:
return
if pressed == key.CTRL_W:
self.current_text = ""
elif pressed == key.BACKSPACE:
if self.current_text:
self.current_text = self.current_text[:-1]
elif len(pressed) != 1:
return
else:
self.current_text += pressed

if prev_text != self.current_text:
self.current = 0
if not self.current_text:
self.question.remove_filter()
else:
self.question.apply_filter(self._filter_func)

def _filter_func(self, choices):
return self.question.filter_func(self.current_text, choices)

def _current_index(self):
try:
return self.question.choices.index(self.question.default)
except ValueError:
return 0
Loading