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_

Discussion:
viniciusdc: Hi @pmeier, I know you were also interested in this addition. Feel free to push and update the PR as needed.
viniciusdc: Hi @kiancross, sorry for the delay with the PR. Would you mind approving the CI tests to run?  Thanks in advance 😄 
viniciusdc: Hi @kiancross, sorry for the constant pings. Just wanted to know if you could have a look into this whenever you have some time 😄 Happy holidays 🎄 
pmeier: @kiancross Maintainer action is required to run CI:

![image](https://github.com/tmbo/questionary/assets/6849766/f2e65b99-68b0-4105-8b04-2f11d1a42704)

kiancross: CI is running now. But some of the tests are failing!
pmeier: @kiancross could you approve CI again and take another look? Locally, everything is passing for me after 9305d578db0b989f2eefb9880d3f6db413eaa818.
pmeier: @kiancross Anything left to do now that CI is passing or can this be merged?

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