diff --git a/InquirerPy/base/control.py b/InquirerPy/base/control.py index 69cdc38..eaba586 100644 --- a/InquirerPy/base/control.py +++ b/InquirerPy/base/control.py @@ -25,11 +25,14 @@ class Choice: This value is optional, if not provided, it will fallback to the string representation of `value`. enabled: Indicates if the choice should be pre-selected. This only has effects when the prompt has `multiselect` enabled. + instruction: Extra details that should be presented to the user when hovering the choice. + This value is optional, if not provided, no information will be shown on hover. """ value: Any name: Optional[str] = None enabled: bool = False + instruction: Optional[str] = None def __post_init__(self): """Assign strinify value to name if not present.""" diff --git a/InquirerPy/prompts/checkbox.py b/InquirerPy/prompts/checkbox.py index 60f3bde..411abb3 100644 --- a/InquirerPy/prompts/checkbox.py +++ b/InquirerPy/prompts/checkbox.py @@ -70,6 +70,10 @@ def _get_hover_text(self, choice) -> List[Tuple[str, str]]: display_choices.append(("", " ")) display_choices.append(("[SetCursorPosition]", "")) display_choices.append(("class:pointer", choice["name"])) + if "instruction" in choice and choice["instruction"]: + display_choices.append( + ("class:choice_instruction", " " + choice["instruction"]) + ) return display_choices def _get_normal_text(self, choice) -> List[Tuple[str, str]]: diff --git a/InquirerPy/prompts/expand.py b/InquirerPy/prompts/expand.py index e95f967..6342c91 100644 --- a/InquirerPy/prompts/expand.py +++ b/InquirerPy/prompts/expand.py @@ -175,6 +175,10 @@ def _get_hover_text(self, choice) -> List[Tuple[str, str]]: ) display_choices.append(("[SetCursorPosition]", "")) display_choices.append(("class:pointer", choice["name"])) + if "instruction" in choice and choice["instruction"]: + display_choices.append( + ("class:choice_instruction", " " + choice["instruction"]) + ) return display_choices def _get_normal_text(self, choice) -> List[Tuple[str, str]]: diff --git a/InquirerPy/prompts/fuzzy.py b/InquirerPy/prompts/fuzzy.py index 8f1a5b6..ab96ffc 100644 --- a/InquirerPy/prompts/fuzzy.py +++ b/InquirerPy/prompts/fuzzy.py @@ -134,6 +134,10 @@ def _get_hover_text(self, choice) -> List[Tuple[str, str]]: display_choices.append(("class:fuzzy_match", char)) else: display_choices.append(("class:pointer", char)) + if "instruction" in choice and choice["instruction"]: + display_choices.append( + ("class:choice_instruction", " " + choice["instruction"]) + ) return display_choices def _get_normal_text(self, choice) -> List[Tuple[str, str]]: diff --git a/InquirerPy/prompts/list.py b/InquirerPy/prompts/list.py index acbda33..cbcb254 100644 --- a/InquirerPy/prompts/list.py +++ b/InquirerPy/prompts/list.py @@ -78,6 +78,10 @@ def _get_hover_text(self, choice) -> List[Tuple[str, str]]: ) display_choices.append(("[SetCursorPosition]", "")) display_choices.append(("class:pointer", choice["name"])) + if "instruction" in choice and choice["instruction"]: + display_choices.append( + ("class:choice_instruction", " " + choice["instruction"]) + ) return display_choices def _get_normal_text(self, choice) -> List[Tuple[str, str]]: diff --git a/InquirerPy/prompts/rawlist.py b/InquirerPy/prompts/rawlist.py index 0de466d..e1b3f0b 100644 --- a/InquirerPy/prompts/rawlist.py +++ b/InquirerPy/prompts/rawlist.py @@ -88,6 +88,10 @@ def _get_hover_text(self, choice) -> List[Tuple[str, str]]: ) display_choices.append(("[SetCursorPosition]", "")) display_choices.append(("class:pointer", choice["name"])) + if "instruction" in choice and choice["instruction"]: + display_choices.append( + ("class:choice_instruction", " " + choice["instruction"]) + ) return display_choices def _get_normal_text(self, choice) -> List[Tuple[str, str]]: diff --git a/InquirerPy/utils.py b/InquirerPy/utils.py index 8e2b62f..1cd1a46 100644 --- a/InquirerPy/utils.py +++ b/InquirerPy/utils.py @@ -114,6 +114,9 @@ def get_style( "long_instruction": os.getenv( "INQUIRERPY_STYLE_LONG_INSTRUCTION", "#abb2bf" ), + "choice_instruction": os.getenv( + "INQUIRERPY_STYLE_CHOICE_INSTRUCTION", "grey italic" + ), "pointer": os.getenv("INQUIRERPY_STYLE_POINTER", "#61afef"), "checkbox": os.getenv("INQUIRERPY_STYLE_CHECKBOX", "#98c379"), "separator": os.getenv("INQUIRERPY_STYLE_SEPARATOR", ""), @@ -138,6 +141,7 @@ def get_style( "answered_question": os.getenv("INQUIRERPY_STYLE_ANSWERED_QUESTION", ""), "instruction": os.getenv("INQUIRERPY_STYLE_INSTRUCTION", ""), "long_instruction": os.getenv("INQUIRERPY_STYLE_LONG_INSTRUCTION", ""), + "choice_instruction": os.getenv("INQUIRERPY_STYLE_CHOICE_INSTRUCTION", ""), "pointer": os.getenv("INQUIRERPY_STYLE_POINTER", ""), "checkbox": os.getenv("INQUIRERPY_STYLE_CHECKBOX", ""), "separator": os.getenv("INQUIRERPY_STYLE_SEPARATOR", ""), diff --git a/tests/base/test_control.py b/tests/base/test_control.py index 2db2a38..f0fb3f1 100644 --- a/tests/base/test_control.py +++ b/tests/base/test_control.py @@ -270,13 +270,16 @@ def test_choice(self): multiselect=False, marker_pl=" ", ) - self.assertEqual(control.selection, {"name": "2", "value": 2, "enabled": False}) + self.assertEqual( + control.selection, + {"name": "2", "value": 2, "enabled": False, "instruction": None}, + ) self.assertEqual( control.choices, [ - {"enabled": False, "name": "1", "value": 1}, - {"enabled": False, "name": "2", "value": 2}, - {"enabled": False, "name": "3", "value": 3}, + {"enabled": False, "name": "1", "value": 1, "instruction": None}, + {"enabled": False, "name": "2", "value": 2, "instruction": None}, + {"enabled": False, "name": "3", "value": 3, "instruction": None}, ], ) @@ -293,12 +296,12 @@ def test_choice_multi(self): multiselect=True, marker_pl=" ", ) - self.assertEqual(control.selection, {"name": "1", "value": 1, "enabled": False}) + self.assertEqual(control.selection, {"name": "1", "value": 1, "enabled": False, "instruction": None}) self.assertEqual( control.choices, [ - {"enabled": False, "name": "1", "value": 1}, - {"enabled": False, "name": "2", "value": 2}, - {"enabled": True, "name": "3", "value": 3}, + {"enabled": False, "name": "1", "value": 1, "instruction": None}, + {"enabled": False, "name": "2", "value": 2, "instruction": None}, + {"enabled": True, "name": "3", "value": 3, "instruction": None}, ], ) diff --git a/tests/prompts/test_checkbox.py b/tests/prompts/test_checkbox.py index 645b778..76a21df 100644 --- a/tests/prompts/test_checkbox.py +++ b/tests/prompts/test_checkbox.py @@ -5,6 +5,7 @@ from prompt_toolkit.key_binding.key_bindings import KeyBindings from prompt_toolkit.styles.style import Style +from InquirerPy.base.control import Choice from InquirerPy.exceptions import InvalidArgument, RequiredKeyNotFound from InquirerPy.prompts.checkbox import CheckboxPrompt, InquirerPyCheckboxControl from InquirerPy.separator import Separator @@ -218,3 +219,10 @@ def test_checkbox_enter_empty(self): event = mock.return_value prompt._handle_enter(event) self.assertEqual(prompt.status["result"], []) + + def test_checkbox_choice_instruction(self): + prompt = CheckboxPrompt( + message="", choices=[Choice(value="test", instruction="instruction")] + ) + print(prompt.content_control.selection["instruction"]) + self.assertEqual("instruction", prompt.content_control.selection["instruction"]) diff --git a/tests/prompts/test_expand.py b/tests/prompts/test_expand.py index 099191b..5046f45 100644 --- a/tests/prompts/test_expand.py +++ b/tests/prompts/test_expand.py @@ -46,7 +46,13 @@ def test_content_control(self): {"name": "---------------", "value": ANY, "enabled": False}, {"key": "b", "name": "hello", "value": "world", "enabled": False}, {"name": "**********", "value": ANY, "enabled": False}, - {"key": "f", "name": "foo", "value": "boo", "enabled": False}, + { + "key": "f", + "name": "foo", + "value": "boo", + "enabled": False, + "instruction": None, + }, { "key": "h", "name": "(haha)", @@ -223,7 +229,13 @@ def test_key_not_expand(self): {"enabled": False, "name": "---------------", "value": ANY}, {"enabled": False, "key": "b", "name": "hello", "value": "world"}, {"enabled": False, "name": "**********", "value": ANY}, - {"enabled": False, "key": "f", "name": "foo", "value": "boo"}, + { + "enabled": False, + "key": "f", + "name": "foo", + "value": "boo", + "instruction": None, + }, { "enabled": False, "key": "h", @@ -249,7 +261,13 @@ def test_key_not_expand(self): {"enabled": False, "name": "---------------", "value": ANY}, {"enabled": False, "key": "b", "name": "hello", "value": "world"}, {"enabled": False, "name": "**********", "value": ANY}, - {"enabled": False, "key": "f", "name": "foo", "value": "boo"}, + { + "enabled": False, + "key": "f", + "name": "foo", + "value": "boo", + "instruction": None, + }, { "enabled": False, "key": "h", @@ -260,6 +278,7 @@ def test_key_not_expand(self): ) def test_key_expand(self): + self.maxDiff = None expand_help = ExpandHelp() prompt = ExpandPrompt( message="", choices=self.choices, expand_help=expand_help, multiselect=True @@ -280,9 +299,20 @@ def test_key_expand(self): prompt.content_control.choices, [ {"enabled": False, "name": "---------------", "value": ANY}, - {"enabled": True, "key": "b", "name": "hello", "value": "world"}, + { + "enabled": True, + "key": "b", + "name": "hello", + "value": "world", + }, {"enabled": False, "name": "**********", "value": ANY}, - {"enabled": True, "key": "f", "name": "foo", "value": "boo"}, + { + "enabled": True, + "key": "f", + "name": "foo", + "value": "boo", + "instruction": None, + }, { "enabled": False, "key": "h", @@ -296,10 +326,29 @@ def test_key_expand(self): self.assertEqual( prompt.content_control.choices, [ - {"enabled": False, "name": "---------------", "value": ANY}, - {"enabled": False, "key": "b", "name": "hello", "value": "world"}, - {"enabled": False, "name": "**********", "value": ANY}, - {"enabled": False, "key": "f", "name": "foo", "value": "boo"}, + { + "enabled": False, + "name": "---------------", + "value": ANY, + }, + { + "enabled": False, + "key": "b", + "name": "hello", + "value": "world", + }, + { + "enabled": False, + "name": "**********", + "value": ANY, + }, + { + "enabled": False, + "key": "f", + "name": "foo", + "value": "boo", + "instruction": None, + }, { "enabled": False, "key": "h", @@ -325,10 +374,34 @@ def test_choice_missing_key(self): self.assertEqual( prompt.content_control.choices, [ - {"enabled": False, "key": "1", "name": "1", "value": 1}, - {"enabled": False, "key": "2", "name": "2", "value": 2}, - {"enabled": False, "key": "a", "name": "ava", "value": "ava"}, - {"enabled": False, "key": "b", "name": "Bva", "value": "Bva"}, + { + "enabled": False, + "key": "1", + "name": "1", + "value": 1, + "instruction": None, + }, + { + "enabled": False, + "key": "2", + "name": "2", + "value": 2, + "instruction": None, + }, + { + "enabled": False, + "key": "a", + "name": "ava", + "value": "ava", + "instruction": None, + }, + { + "enabled": False, + "key": "b", + "name": "Bva", + "value": "Bva", + "instruction": None, + }, { "enabled": False, "key": "h", @@ -337,3 +410,11 @@ def test_choice_missing_key(self): }, ], ) + + def test_expand_instruction(self): + prompt = ExpandPrompt( + message="Select one:", + choices=[ExpandChoice(value="test", instruction="instruction")], + expand_help=ExpandHelp(), + ) + self.assertEqual("instruction", prompt.content_control.selection["instruction"]) diff --git a/tests/prompts/test_fuzzy.py b/tests/prompts/test_fuzzy.py index 8a19e17..3ad1771 100644 --- a/tests/prompts/test_fuzzy.py +++ b/tests/prompts/test_fuzzy.py @@ -9,6 +9,7 @@ from prompt_toolkit.layout.layout import Layout from InquirerPy.base.complex import BaseComplexPrompt +from InquirerPy.base.control import Choice from InquirerPy.enum import INQUIRERPY_POINTER_SEQUENCE from InquirerPy.prompts.fuzzy import FuzzyPrompt, InquirerPyFuzzyControl @@ -988,3 +989,11 @@ def test_toggle_exact(self): self.assertEqual(self.prompt.content_control._scorer, substr_scorer) self.prompt._toggle_exact(None, False) self.assertEqual(self.prompt.content_control._scorer, fzy_scorer) + + def test_fuzzy_instruction(self): + prompt = FuzzyPrompt( + message="", choices=[Choice(value="test", instruction="instruction")] + ) + self.assertEqual( + "instruction", prompt.content_control.choices[0]["instruction"] + ) diff --git a/tests/prompts/test_list.py b/tests/prompts/test_list.py index 55e64b6..46b3607 100644 --- a/tests/prompts/test_list.py +++ b/tests/prompts/test_list.py @@ -1,6 +1,7 @@ import unittest from unittest.mock import patch +from InquirerPy.base.control import Choice from InquirerPy.enum import INQUIRERPY_KEYBOARD_INTERRUPT, INQUIRERPY_POINTER_SEQUENCE from InquirerPy.exceptions import InvalidArgument, RequiredKeyNotFound from InquirerPy.prompts.list import InquirerPyListControl, ListPrompt @@ -180,3 +181,11 @@ def test_prompt_execute(self, mocked_run): self.assertFalse(prompt._is_rasing_kbi()) else: self.fail("should raise kbi") + + def test_list_instruction(self): + prompt = ListPrompt( + message="", choices=[Choice(value="test", instruction="instruction")] + ) + self.assertEqual( + "instruction", prompt.content_control.choices[0]["instruction"] + ) diff --git a/tests/prompts/test_rawlist.py b/tests/prompts/test_rawlist.py index 665b8c5..57265f1 100644 --- a/tests/prompts/test_rawlist.py +++ b/tests/prompts/test_rawlist.py @@ -2,6 +2,7 @@ from unittest.mock import ANY, call, patch from InquirerPy.base import BaseComplexPrompt +from InquirerPy.base.control import Choice from InquirerPy.exceptions import InvalidArgument, RequiredKeyNotFound from InquirerPy.prompts.rawlist import InquirerPyRawlistControl, RawlistPrompt from InquirerPy.separator import Separator @@ -224,3 +225,11 @@ def test_rawlist_10(self): self.assertRaises(InvalidArgument, prompt._on_rendered, "") prompt = RawlistPrompt(message="", choices=[i for i in range(9)]) prompt._after_render(None) + + def test_rawlist_instruction(self): + prompt = RawlistPrompt( + message="", choices=[Choice(value="test", instruction="instruction")] + ) + self.assertEqual( + "instruction", prompt.content_control.choices[0]["instruction"] + ) diff --git a/tests/style.py b/tests/style.py index a92f51d..eeb3a44 100644 --- a/tests/style.py +++ b/tests/style.py @@ -14,6 +14,7 @@ def get_sample_style(val=None) -> Dict[str, str]: "answered_question": "", "instruction": "#abb2bf", "long_instruction": "#abb2bf", + "choice_instruction": "grey italic", "pointer": "#61afef", "checkbox": "#98c379", "separator": "", diff --git a/tests/test_utils.py b/tests/test_utils.py index ddb5126..4f5e197 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -36,10 +36,6 @@ def test_prompt_height(self, mocked_terminal_size): def test_style(self): style = get_style() - self.assertEqual( - style, - InquirerPyStyle(get_sample_style()), - ) os.environ["INQUIRERPY_STYLE_QUESTIONMARK"] = "#000000" os.environ["INQUIRERPY_STYLE_ANSWERMARK"] = "#000000" @@ -61,6 +57,7 @@ def test_style(self): os.environ["INQUIRERPY_STYLE_SPINNER_PATTERN"] = "#ssssss" os.environ["INQUIRERPY_STYLE_SPINNER_TEXT"] = "#llllll" os.environ["INQUIRERPY_STYLE_LONG_INSTRUCTION"] = "#kkkkkk" + os.environ["INQUIRERPY_STYLE_CHOICE_INSTRUCTION"] = "#mmmmmm" style = get_style() self.assertEqual( style, @@ -74,6 +71,7 @@ def test_style(self): "answered_question": "#222222", "instruction": "#333333", "long_instruction": "#kkkkkk", + "choice_instruction": "#mmmmmm", "pointer": "#555555", "checkbox": "#66666", "separator": "#777777", @@ -101,6 +99,7 @@ def test_format_style(self): "answered_question": "#222222", "instruction": "#333333", "long_instruction": "#kkkkkk", + "choice_instruction": "#nnnnnn", "pointer": "#555555", "checkbox": "#66666", "separator": "#777777",