diff --git a/docs/usage.md b/docs/usage.md index c9e2fc9c..af2be08e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -133,6 +133,16 @@ choices = { question = questions.Checkbox("foo", "Choose one:", choices=choices.keys(), hints=choices) ``` +### max_options_displayed_at_once + +**Optional** for `Checkbox` and `List` questions. When `None` (default), at most 13 choices are visible at once and the list scrolls around the cursor. Set it to a positive integer to display more (or fewer) choices in the visible window. + +```python +from inquirer import questions +choices = [f"item-{i}" for i in range(200)] +question = questions.List("pick", "Pick one:", choices=choices, max_options_displayed_at_once=25) +``` + ### validate Optional attribute that allows the program to check if the answer is valid or not. It requires a `boolean` value or a `function` with the signature: diff --git a/src/inquirer/questions.py b/src/inquirer/questions.py index 25495720..9e57d75f 100644 --- a/src/inquirer/questions.py +++ b/src/inquirer/questions.py @@ -157,10 +157,12 @@ def __init__( carousel=False, other=False, autocomplete=None, + max_options_displayed_at_once=None, ): super().__init__(name, message, choices, default, ignore, validate, hints=hints, other=other) self.carousel = carousel self.autocomplete = autocomplete + self.max_options_displayed_at_once = max_options_displayed_at_once class Checkbox(Question): @@ -179,11 +181,13 @@ def __init__( carousel=False, other=False, autocomplete=None, + max_options_displayed_at_once=None, ): super().__init__(name, message, choices, default, ignore, validate, hints=hints, other=other) self.locked = locked self.carousel = carousel self.autocomplete = autocomplete + self.max_options_displayed_at_once = max_options_displayed_at_once class Path(Text): diff --git a/src/inquirer/render/console/_checkbox.py b/src/inquirer/render/console/_checkbox.py index 6e40c5bd..94c5c5ac 100644 --- a/src/inquirer/render/console/_checkbox.py +++ b/src/inquirer/render/console/_checkbox.py @@ -4,7 +4,6 @@ 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 Checkbox(BaseConsoleRender): @@ -25,22 +24,33 @@ def default_choices(self): default = self.question.default or [] return default + self.locked + @property + def max_options_displayed(self): + value = getattr(self.question, "max_options_displayed_at_once", None) + return value if value else MAX_OPTIONS_DISPLAYED_AT_ONCE + + @property + def half_options(self): + return int((self.max_options_displayed - 1) / 2) + @property def is_long(self): choices = self.question.choices or [] - return len(choices) >= MAX_OPTIONS_DISPLAYED_AT_ONCE + return len(choices) >= self.max_options_displayed def get_options(self): choices = self.question.choices or [] + max_options = self.max_options_displayed + half_options = self.half_options if self.is_long: cmin = 0 - cmax = MAX_OPTIONS_DISPLAYED_AT_ONCE + cmax = max_options 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 + cmin += len(choices) - max_options cmax += len(choices) cchoices = choices[cmin:cmax] @@ -55,7 +65,7 @@ def get_options(self): if ( (is_in_middle and self.current - half_options + index in self.selection) or (is_in_beginning and index in self.selection) - or (is_in_end and index + max(len(choices) - MAX_OPTIONS_DISPLAYED_AT_ONCE, 0) in self.selection) + or (is_in_end and index + max(len(choices) - max_options, 0) in self.selection) ): # noqa symbol = self.theme.Checkbox.selected_icon color = self.theme.Checkbox.selected_color diff --git a/src/inquirer/render/console/_list.py b/src/inquirer/render/console/_list.py index 38ac2baa..e7f6fac3 100644 --- a/src/inquirer/render/console/_list.py +++ b/src/inquirer/render/console/_list.py @@ -4,7 +4,6 @@ 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 List(BaseConsoleRender): @@ -12,10 +11,19 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.current = self._current_index() + @property + def max_options_displayed(self): + value = getattr(self.question, "max_options_displayed_at_once", None) + return value if value else MAX_OPTIONS_DISPLAYED_AT_ONCE + + @property + def half_options(self): + return int((self.max_options_displayed - 1) / 2) + @property def is_long(self): choices = self.question.choices or [] - return len(choices) >= MAX_OPTIONS_DISPLAYED_AT_ONCE + return len(choices) >= self.max_options_displayed def get_hint(self): try: @@ -30,15 +38,17 @@ def get_hint(self): def get_options(self): choices = self.question.choices or [] + max_options = self.max_options_displayed + half_options = self.half_options if self.is_long: cmin = 0 - cmax = MAX_OPTIONS_DISPLAYED_AT_ONCE + cmax = max_options 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 + cmin += len(choices) - max_options cmax += len(choices) cchoices = choices[cmin:cmax] diff --git a/tests/integration/console_render/test_checkbox.py b/tests/integration/console_render/test_checkbox.py index f1531971..454ff6c2 100644 --- a/tests/integration/console_render/test_checkbox.py +++ b/tests/integration/console_render/test_checkbox.py @@ -428,3 +428,33 @@ def test_second_hint_is_shown(self): sut.render(question) self.assertInStdout("Bar") + + def test_max_options_displayed_at_once_shows_more_choices(self): + stdin = helper.event_factory(key.ENTER) + message = "Pick some" + variable = "many" + choices = [f"opt-{i:03d}" for i in range(40)] + + question = questions.Checkbox( + variable, message, choices=choices, max_options_displayed_at_once=25 + ) + sut = ConsoleRender(event_generator=stdin) + sut.render(question) + + self.assertInStdout("opt-000") + self.assertInStdout("opt-024") + self.assertNotInStdout("opt-025") + + def test_max_options_displayed_at_once_default_unchanged(self): + stdin = helper.event_factory(key.ENTER) + message = "Pick some" + variable = "many" + choices = [f"opt-{i:03d}" for i in range(40)] + + question = questions.Checkbox(variable, message, choices=choices) + sut = ConsoleRender(event_generator=stdin) + sut.render(question) + + self.assertInStdout("opt-000") + self.assertInStdout("opt-012") + self.assertNotInStdout("opt-013") diff --git a/tests/integration/console_render/test_list.py b/tests/integration/console_render/test_list.py index 7cd3dea7..7244db10 100644 --- a/tests/integration/console_render/test_list.py +++ b/tests/integration/console_render/test_list.py @@ -181,3 +181,33 @@ def test_taggedValue_with_dict(self): sut.render(question) self.assertInStdout("bb") + + def test_max_options_displayed_at_once_shows_more_choices(self): + stdin = helper.event_factory(key.ENTER) + message = "Pick one" + variable = "many" + choices = [f"opt-{i:03d}" for i in range(40)] + + question = questions.List( + variable, message, choices=choices, max_options_displayed_at_once=25 + ) + sut = ConsoleRender(event_generator=stdin) + sut.render(question) + + self.assertInStdout("opt-000") + self.assertInStdout("opt-024") + self.assertNotInStdout("opt-025") + + def test_max_options_displayed_at_once_default_unchanged(self): + stdin = helper.event_factory(key.ENTER) + message = "Pick one" + variable = "many" + choices = [f"opt-{i:03d}" for i in range(40)] + + question = questions.List(variable, message, choices=choices) + sut = ConsoleRender(event_generator=stdin) + sut.render(question) + + self.assertInStdout("opt-000") + self.assertInStdout("opt-012") + self.assertNotInStdout("opt-013")