Source code for questionary.prompts.checkbox

from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, 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, merge_styles
from prompt_toolkit.formatted_text import FormattedText

from questionary import utils
from questionary.constants import (
    DEFAULT_QUESTION_PREFIX,
    DEFAULT_SELECTED_POINTER,
    DEFAULT_STYLE,
    INVALID_INPUT,
)
from questionary.prompts import common
from questionary.prompts.common import Choice, InquirerControl, Separator
from questionary.question import Question


[docs]def checkbox( message: str, choices: Sequence[Union[str, Choice, Dict[str, Any]]], default: Optional[str] = None, validate: Callable[[List[str]], Union[bool, str]] = lambda a: True, qmark: str = DEFAULT_QUESTION_PREFIX, pointer: Optional[str] = DEFAULT_SELECTED_POINTER, style: Optional[Style] = None, initial_choice: Optional[Union[str, Choice, Dict[str, Any]]] = None, use_arrow_keys: bool = True, use_jk_keys: bool = True, **kwargs: Any, ) -> Question: """Ask the user to select from a list of items. This is a multiselect, the user can choose one, none or many of the items. Example: >>> import questionary >>> questionary.checkbox( ... 'Select toppings', ... choices=[ ... "Cheese", ... "Tomato", ... "Pineapple", ... ]).ask() ? Select toppings done (2 selections) ['Cheese', 'Pineapple'] .. image:: ../images/checkbox.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: Default return value (single value). If you want to preselect multiple items, use ``Choice("foo", checked=True)`` instead. validate: Require the entered value to pass a validation. The value can not be submitted until the validator accepts it (e.g. to check minimum password length). This should be a function accepting the input and returning a boolean. Alternatively, the return value may be a string (indicating failure), which contains the error message to be displayed. 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. style: A custom color and style for the question parts. You can configure colors as well as font types for different elements. initial_choice: A value corresponding to a selectable item in the choices, to initially set the pointer position to. use_arrow_keys: Allow the user to select items from the list using arrow keys. use_jk_keys: Allow the user to select items from the list using `j` (down) and `k` (up) keys. Returns: :class:`Question`: Question instance, ready to be prompted (using ``.ask()``). """ if not (use_arrow_keys or use_jk_keys): raise ValueError( "Some option to move the selection is required. Arrow keys or j/k keys." ) merged_style = merge_styles( [ DEFAULT_STYLE, # Disable the default inverted colours bottom-toolbar behaviour (for # the error message). However it can be re-enabled with a custom # style. Style([("bottom-toolbar", "noreverse")]), style, ] ) if not callable(validate): raise ValueError("validate must be callable") ic = InquirerControl( choices, default, pointer=pointer, initial_choice=initial_choice ) def get_prompt_tokens() -> List[Tuple[str, str]]: tokens = [] tokens.append(("class:qmark", qmark)) tokens.append(("class:question", " {} ".format(message))) if ic.is_answered: nbr_selected = len(ic.selected_options) if nbr_selected == 0: tokens.append(("class:answer", "done")) elif nbr_selected == 1: if isinstance(ic.get_selected_values()[0].title, list): ts = ic.get_selected_values()[0].title tokens.append( ( "class:answer", "".join([token[1] for token in ts]), # type:ignore ) ) else: tokens.append( ( "class:answer", "[{}]".format(ic.get_selected_values()[0].title), ) ) else: tokens.append( ("class:answer", "done ({} selections)".format(nbr_selected)) ) else: tokens.append( ( "class:instruction", "(Use arrow keys to move, " "<space> to select, " "<a> to toggle, " "<i> to invert)", ) ) return tokens def get_selected_values() -> List[Any]: return [c.value for c in ic.get_selected_values()] def perform_validation(selected_values: List[str]) -> bool: verdict = validate(selected_values) valid = verdict is True if not valid: if verdict is False: error_text = INVALID_INPUT else: error_text = str(verdict) error_message = FormattedText([("class:validation-toolbar", error_text)]) ic.error_message = ( error_message if not valid and ic.submission_attempted else None ) return valid 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") @bindings.add(" ", eager=True) def toggle(_event): pointed_choice = ic.get_pointed_at().value if pointed_choice in ic.selected_options: ic.selected_options.remove(pointed_choice) else: ic.selected_options.append(pointed_choice) perform_validation(get_selected_values()) @bindings.add("i", eager=True) def invert(_event): inverted_selection = [ c.value for c in ic.choices if not isinstance(c, Separator) and c.value not in ic.selected_options and not c.disabled ] ic.selected_options = inverted_selection perform_validation(get_selected_values()) @bindings.add("a", eager=True) def all(_event): all_selected = True # all choices have been selected for c in ic.choices: if ( not isinstance(c, Separator) and c.value not in ic.selected_options and not c.disabled ): # add missing ones ic.selected_options.append(c.value) all_selected = False if all_selected: ic.selected_options = [] perform_validation(get_selected_values()) 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) @bindings.add(Keys.ControlM, eager=True) def set_answer(event): selected_values = get_selected_values() ic.submission_attempted = True if perform_validation(selected_values): ic.is_answered = True event.app.exit(result=selected_values) @bindings.add(Keys.Any) def other(_event): """Disallow inserting other text. """ pass return Question( Application( layout=layout, key_bindings=bindings, style=merged_style, **utils.used_kwargs(kwargs, Application.__init__), ) )