# -*- coding: utf-8 -*- from typing import Any from typing import Dict from typing import Optional from typing import Sequence from typing import Union from prompt_toolkit.application import Application from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.keys import Keys from prompt_toolkit.styles import Style from questionary import utils from questionary.constants import DEFAULT_QUESTION_PREFIX from questionary.constants import DEFAULT_SELECTED_POINTER from questionary.prompts import common from questionary.prompts.common import Choice from questionary.prompts.common import InquirerControl from questionary.prompts.common import Separator from questionary.question import Question from questionary.styles import merge_styles_default def select( message: str, choices: Sequence[Union[str, Choice, Dict[str, Any]]], default: Optional[Union[str, Choice, Dict[str, Any]]] = None, qmark: str = DEFAULT_QUESTION_PREFIX, pointer: Optional[str] = DEFAULT_SELECTED_POINTER, style: Optional[Style] = None, use_shortcuts: bool = False, use_arrow_keys: bool = True, use_indicator: bool = False, use_jk_keys: bool = True, use_emacs_keys: bool = True, show_selected: bool = False, instruction: Optional[str] = None, **kwargs: Any, ) -> Question: """A list of items to select **one** option from. The user can pick one option and confirm it (if you want to allow the user to select multiple options, use :meth:`questionary.checkbox` instead). Example: >>> import questionary >>> questionary.select( ... "What do you want to do?", ... choices=[ ... "Order a pizza", ... "Make a reservation", ... "Ask for opening hours" ... ]).ask() ? What do you want to do? Order a pizza 'Order a pizza' .. image:: ../images/select.gif This is just a really basic example, the prompt can be customised using the parameters. Args: message: Question text choices: Items shown in the selection, this can contain :class:`Choice` or or :class:`Separator` objects or simple items as strings. Passing :class:`Choice` objects, allows you to configure the item more (e.g. preselecting it or disabling it). default: A value corresponding to a selectable item in the choices, to initially set the pointer position to. qmark: Question prefix displayed in front of the question. By default this is a ``?``. pointer: Pointer symbol in front of the currently highlighted element. By default this is a ``ยป``. Use ``None`` to disable it. instruction: A hint on how to navigate the menu. It's ``(Use shortcuts)`` if only ``use_shortcuts`` is set to True, ``(Use arrow keys or shortcuts)`` if ``use_arrow_keys`` & ``use_shortcuts`` are set and ``(Use arrow keys)`` by default. style: A custom color and style for the question parts. You can configure colors as well as font types for different elements. use_indicator: Flag to enable the small indicator in front of the list highlighting the current location of the selection cursor. use_shortcuts: Allow the user to select items from the list using shortcuts. The shortcuts will be displayed in front of the list items. Arrow keys, j/k keys and shortcuts are not mutually exclusive. use_arrow_keys: Allow the user to select items from the list using arrow keys. Arrow keys, j/k keys and shortcuts are not mutually exclusive. use_jk_keys: Allow the user to select items from the list using `j` (down) and `k` (up) keys. Arrow keys, j/k keys and shortcuts are not mutually exclusive. use_emacs_keys: Allow the user to select items from the list using `Ctrl+N` (down) and `Ctrl+P` (up) keys. Arrow keys, j/k keys, emacs keys and shortcuts are not mutually exclusive. show_selected: Display current selection choice at the bottom of list. Returns: :class:`Question`: Question instance, ready to be prompted (using ``.ask()``). """ if not (use_arrow_keys or use_shortcuts or use_jk_keys or use_emacs_keys): raise ValueError( ( "Some option to move the selection is required. " "Arrow keys, j/k keys, emacs keys, or shortcuts." ) ) if use_shortcuts and use_jk_keys: if any(getattr(c, "shortcut_key", "") in ["j", "k"] for c in choices): raise ValueError( "A choice is trying to register j/k as a " "shortcut key when they are in use as arrow keys " "disable one or the other." ) if choices is None or len(choices) == 0: raise ValueError("A list of choices needs to be provided.") if use_shortcuts and len(choices) > len(InquirerControl.SHORTCUT_KEYS): raise ValueError( "A list with shortcuts supports a maximum of {} " "choices as this is the maximum number " "of keyboard shortcuts that are available. You" "provided {} choices!" "".format(len(InquirerControl.SHORTCUT_KEYS), len(choices)) ) merged_style = merge_styles_default([style]) ic = InquirerControl( choices, default, pointer=pointer, use_indicator=use_indicator, use_shortcuts=use_shortcuts, show_selected=show_selected, use_arrow_keys=use_arrow_keys, initial_choice=default, ) def get_prompt_tokens(): # noinspection PyListCreation tokens = [("class:qmark", qmark), ("class:question", " {} ".format(message))] if ic.is_answered: if isinstance(ic.get_pointed_at().title, list): tokens.append( ( "class:answer", "".join([token[1] for token in ic.get_pointed_at().title]), ) ) else: tokens.append(("class:answer", ic.get_pointed_at().title)) else: if instruction: tokens.append(("class:instruction", instruction)) else: if use_shortcuts and use_arrow_keys: instruction_msg = "(Use shortcuts or arrow keys)" elif use_shortcuts and not use_arrow_keys: instruction_msg = "(Use shortcuts)" else: instruction_msg = "(Use arrow keys)" tokens.append(("class:instruction", instruction_msg)) return tokens layout = common.create_inquirer_layout(ic, get_prompt_tokens, **kwargs) bindings = KeyBindings() @bindings.add(Keys.ControlQ, eager=True) @bindings.add(Keys.ControlC, eager=True) def _(event): event.app.exit(exception=KeyboardInterrupt, style="class:aborting") if use_shortcuts: # add key bindings for choices for i, c in enumerate(ic.choices): if c.shortcut_key is None and not c.disabled and not use_arrow_keys: raise RuntimeError( "{} does not have a shortcut and arrow keys " "for movement are disabled. " "This choice is not reachable.".format(c.title) ) if isinstance(c, Separator) or c.shortcut_key is None: continue # noinspection PyShadowingNames def _reg_binding(i, keys): # trick out late evaluation with a "function factory": # https://stackoverflow.com/a/3431699 @bindings.add(keys, eager=True) def select_choice(event): ic.pointed_at = i _reg_binding(i, c.shortcut_key) def move_cursor_down(event): ic.select_next() while not ic.is_selection_valid(): ic.select_next() def move_cursor_up(event): ic.select_previous() while not ic.is_selection_valid(): ic.select_previous() if use_arrow_keys: bindings.add(Keys.Down, eager=True)(move_cursor_down) bindings.add(Keys.Up, eager=True)(move_cursor_up) if use_jk_keys: bindings.add("j", eager=True)(move_cursor_down) bindings.add("k", eager=True)(move_cursor_up) if use_emacs_keys: bindings.add(Keys.ControlN, eager=True)(move_cursor_down) bindings.add(Keys.ControlP, eager=True)(move_cursor_up) @bindings.add(Keys.ControlM, eager=True) def set_answer(event): ic.is_answered = True event.app.exit(result=ic.get_pointed_at().value) @bindings.add(Keys.Any) def other(event): """Disallow inserting other text.""" return Question( Application( layout=layout, key_bindings=bindings, style=merged_style, **utils.used_kwargs(kwargs, Application.__init__), ) )