Repository: tmbo/questionary

Issue:
[ENH] Include `show_description` to `InquirerControl`
### Describe the problem

Well done on the project! I've been utilizing it extensively for dynamic CLI design.

Sometimes, CLI or workflow questions can be intricate, and supplying additional information regarding options can substantially enhance the user experience and effectiveness. I suggest integrating a `show_description` option into the `InquirerControl` class. This option would allow developers to include a brief description of the presently selected option, which would appear at the bottom of the current question, akin to the show_selected feature.

Integrating this option would enable users to rapidly comprehend each option's objective and make informed decisions, leading to a more user-friendly and efficient interface. This would aid in creating more insightful interactions within CLI or advanced workflows without any detrimental consequences.

For example, using the sample example from [select docs](https://questionary.readthedocs.io/en/stable/pages/types.html#select), we could have something as:
```python
import questionary

choice_options = [
      {
          "name": "Medium", 
          "value": "$15", 
          "description": "Available toppings: cheese, pepperoni, sausage"
       },
      {
          "name": "Big", 
          "value": "$20", 
          "description": "Available toppings: cheese, mushrooms, onions, peppers, pineapple, ham"
       },
]

question = select(
    "Select a pizza size: (see descriptions for toppings)",
    choices=choice_options,
    show_descriptions=True,
).ask()
```
![Peek 11-04-2023 14-33](https://user-images.githubusercontent.com/51954708/231243348-8fa4866e-6414-4c59-a49f-734c21109c41.gif)



### Describe the solution

- Extend Choices to receive the extra `description` attribute
- Extend `questionary.select` and `InquirerControl` to accept the new `show_description` flag

### Alternatives considered

_No response_

Hints from issue comments:
@viniciusdc: I am willing to contribute with a PR if this addition looks reasonable. @viniciusdc: @kiancross I pinging you here cause I saw you were the latest one to merge a PR here. I would love for this to be included as well, and I can have a PR done too. Let me know what are your thoughts. @kiancross: Sorry for the slow reply. This looks reasonable, and a PR would be very much appreciated! Please make sure it is backwards compatible, and tests are included :)@viniciusdc: Hi @kiancross, thanks for having a look. We've created a PR with the suggested enhancements, and its ready for review :smile: 

Solution patch:
diff --git a/questionary/prompts/checkbox.py b/questionary/prompts/checkbox.py
index 9523465..25161e4 100644
--- a/questionary/prompts/checkbox.py
+++ b/questionary/prompts/checkbox.py
@@ -38,6 +38,7 @@ def checkbox(
     use_jk_keys: bool = True,
     use_emacs_keys: bool = True,
     instruction: Optional[str] = None,
+    show_description: bool = True,
     **kwargs: Any,
 ) -> Question:
     """Ask the user to select from a list of items.
@@ -106,6 +107,8 @@ def checkbox(
                         `Ctrl+N` (down) and `Ctrl+P` (up) keys.
         instruction: A message describing how to navigate the menu.
 
+        show_description: Display description of current selection if available.
+
     Returns:
         :class:`Question`: Question instance, ready to be prompted (using ``.ask()``).
     """
@@ -130,7 +133,11 @@ def checkbox(
         raise ValueError("validate must be callable")
 
     ic = InquirerControl(
-        choices, default, pointer=pointer, initial_choice=initial_choice
+        choices,
+        default,
+        pointer=pointer,
+        initial_choice=initial_choice,
+        show_description=show_description,
     )
 
     def get_prompt_tokens() -> List[Tuple[str, str]]:
diff --git a/questionary/prompts/common.py b/questionary/prompts/common.py
index e682f48..827629d 100644
--- a/questionary/prompts/common.py
+++ b/questionary/prompts/common.py
@@ -53,6 +53,8 @@ class Choice:
         checked: Preselect this choice when displaying the options.
 
         shortcut_key: Key shortcut used to select this item.
+
+        description: Optional description of the item that can be displayed.
     """
 
     title: FormattedText
@@ -70,6 +72,9 @@ class Choice:
     shortcut_key: Optional[str]
     """A shortcut key for the choice"""
 
+    description: Optional[str]
+    """Choice description"""
+
     def __init__(
         self,
         title: FormattedText,
@@ -77,10 +82,12 @@ class Choice:
         disabled: Optional[str] = None,
         checked: Optional[bool] = False,
         shortcut_key: Optional[Union[str, bool]] = True,
+        description: Optional[str] = None,
     ) -> None:
         self.disabled = disabled
         self.title = title
         self.checked = checked if checked is not None else False
+        self.description = description
 
         if value is not None:
             self.value = value
@@ -124,6 +131,7 @@ class Choice:
                 c.get("disabled", None),
                 c.get("checked"),
                 c.get("key"),
+                c.get("description", None),
             )
 
     def get_shortcut_title(self):
@@ -202,6 +210,7 @@ class InquirerControl(FormattedTextControl):
     pointer: Optional[str]
     pointed_at: int
     is_answered: bool
+    show_description: bool
 
     def __init__(
         self,
@@ -211,6 +220,7 @@ class InquirerControl(FormattedTextControl):
         use_indicator: bool = True,
         use_shortcuts: bool = False,
         show_selected: bool = False,
+        show_description: bool = True,
         use_arrow_keys: bool = True,
         initial_choice: Optional[Union[str, Choice, Dict[str, Any]]] = None,
         **kwargs: Any,
@@ -218,6 +228,7 @@ class InquirerControl(FormattedTextControl):
         self.use_indicator = use_indicator
         self.use_shortcuts = use_shortcuts
         self.show_selected = show_selected
+        self.show_description = show_description
         self.use_arrow_keys = use_arrow_keys
         self.default = default
         self.pointer = pointer
@@ -417,9 +428,9 @@ class InquirerControl(FormattedTextControl):
         for i, c in enumerate(self.choices):
             append(i, c)
 
-        if self.show_selected:
-            current = self.get_pointed_at()
+        current = self.get_pointed_at()
 
+        if self.show_selected:
             answer = current.get_shortcut_title() if self.use_shortcuts else ""
 
             answer += (
@@ -427,8 +438,16 @@ class InquirerControl(FormattedTextControl):
             )
 
             tokens.append(("class:text", "  Answer: {}".format(answer)))
-        else:
+
+        show_description = self.show_description and current.description is not None
+        if show_description:
+            tokens.append(
+                ("class:text", "  Description: {}".format(current.description))
+            )
+
+        if not (self.show_selected or show_description):
             tokens.pop()  # Remove last newline.
+
         return tokens
 
     def is_selection_a_separator(self) -> bool:
diff --git a/questionary/prompts/select.py b/questionary/prompts/select.py
index d6d41c5..402acbf 100644
--- a/questionary/prompts/select.py
+++ b/questionary/prompts/select.py
@@ -35,6 +35,7 @@ def select(
     use_jk_keys: bool = True,
     use_emacs_keys: bool = True,
     show_selected: bool = False,
+    show_description: bool = True,
     instruction: Optional[str] = None,
     **kwargs: Any,
 ) -> Question:
@@ -110,6 +111,8 @@ def select(
 
         show_selected: Display current selection choice at the bottom of list.
 
+        show_description: Display description of current selection if available.
+
     Returns:
         :class:`Question`: Question instance, ready to be prompted (using ``.ask()``).
     """
@@ -150,6 +153,7 @@ def select(
         use_indicator=use_indicator,
         use_shortcuts=use_shortcuts,
         show_selected=show_selected,
+        show_description=show_description,
         use_arrow_keys=use_arrow_keys,
         initial_choice=default,
     )
@@ -200,7 +204,7 @@ def select(
                     "for movement are disabled. "
                     "This choice is not reachable.".format(c.title)
                 )
-            if isinstance(c, Separator) or c.shortcut_key is None:
+            if isinstance(c, Separator) or c.shortcut_key is None or c.disabled:
                 continue
 
             # noinspection PyShadowingNames


Test patch:
diff --git a/tests/prompts/test_common.py b/tests/prompts/test_common.py
index d6ce51e..3250079 100644
--- a/tests/prompts/test_common.py
+++ b/tests/prompts/test_common.py
@@ -222,3 +222,43 @@ def test_print_with_style(monkeypatch):
 
     assert mock.method_calls[1][0] == "write"
     assert mock.method_calls[1][1][0] == "Hello World"
+
+
+def test_prompt_show_description():
+    ic = InquirerControl(
+        ["a", Choice("b", description="B")],
+        show_selected=True,
+        show_description=True,
+    )
+
+    expected_tokens = [
+        ("class:pointer", " » "),
+        ("[SetCursorPosition]", ""),
+        ("class:text", "○ "),
+        ("class:highlighted", "a"),
+        ("", "\n"),
+        ("class:text", "   "),
+        ("class:text", "○ "),
+        ("class:text", "b"),
+        ("", "\n"),
+        ("class:text", "  Answer: a"),
+    ]
+    assert ic.pointed_at == 0
+    assert ic._get_choice_tokens() == expected_tokens
+
+    ic.select_next()
+    expected_tokens = [
+        ("class:text", "   "),
+        ("class:text", "○ "),
+        ("class:text", "a"),
+        ("", "\n"),
+        ("class:pointer", " » "),
+        ("[SetCursorPosition]", ""),
+        ("class:text", "○ "),
+        ("class:highlighted", "b"),
+        ("", "\n"),
+        ("class:text", "  Answer: b"),
+        ("class:text", "  Description: B"),
+    ]
+    assert ic.pointed_at == 1
+    assert ic._get_choice_tokens() == expected_tokens