from typing import Any from typing import Callable from typing import Dict from typing import Iterable from typing import List from typing import Optional from typing import Tuple from typing import Union from prompt_toolkit.completion import CompleteEvent from prompt_toolkit.completion import Completer from prompt_toolkit.completion import Completion from prompt_toolkit.document import Document from prompt_toolkit.formatted_text import HTML from prompt_toolkit.lexers import SimpleLexer from prompt_toolkit.shortcuts.prompt import CompleteStyle from prompt_toolkit.shortcuts.prompt import PromptSession from prompt_toolkit.styles import Style from questionary.constants import DEFAULT_QUESTION_PREFIX from questionary.prompts.common import build_validator from questionary.question import Question from questionary.styles import merge_styles_default class WordCompleter(Completer): choices_source: Union[List[str], Callable[[], List[str]]] ignore_case: bool meta_information: Dict[str, Any] match_middle: bool def __init__( self, choices: Union[List[str], Callable[[], List[str]]], ignore_case: bool = True, meta_information: Optional[Dict[str, Any]] = None, match_middle: bool = True, ) -> None: self.choices_source = choices self.ignore_case = ignore_case self.meta_information = meta_information or {} self.match_middle = match_middle def _choices(self) -> Iterable[str]: return ( self.choices_source() if callable(self.choices_source) else self.choices_source ) def _choice_matches(self, word_before_cursor: str, choice: str) -> int: """Match index if found, -1 if not.""" if self.ignore_case: choice = choice.lower() if self.match_middle: return choice.find(word_before_cursor) elif choice.startswith(word_before_cursor): return 0 else: return -1 @staticmethod def _display_for_choice(choice: str, index: int, word_before_cursor: str) -> HTML: return HTML("{}{}{}").format( choice[:index], choice[index : index + len(word_before_cursor)], # noqa: E203 choice[index + len(word_before_cursor) : len(choice)], # noqa: E203 ) def get_completions( self, document: Document, complete_event: CompleteEvent ) -> Iterable[Completion]: choices = self._choices() # Get word/text before cursor. word_before_cursor = document.text_before_cursor if self.ignore_case: word_before_cursor = word_before_cursor.lower() for choice in choices: index = self._choice_matches(word_before_cursor, choice) if index == -1: # didn't find a match continue display_meta = self.meta_information.get(choice, "") display = self._display_for_choice(choice, index, word_before_cursor) yield Completion( choice, start_position=-len(choice), display=display.formatted_text, display_meta=display_meta, style="class:answer", selected_style="class:selected", ) def autocomplete( message: str, choices: List[str], default: str = "", qmark: str = DEFAULT_QUESTION_PREFIX, completer: Optional[Completer] = None, meta_information: Optional[Dict[str, Any]] = None, ignore_case: bool = True, match_middle: bool = True, complete_style: CompleteStyle = CompleteStyle.COLUMN, validate: Any = None, style: Optional[Style] = None, **kwargs: Any, ) -> Question: """Prompt the user to enter a message with autocomplete help. Example: >>> import questionary >>> questionary.autocomplete( ... 'Choose ant specie', ... choices=[ ... 'Camponotus pennsylvanicus', ... 'Linepithema humile', ... 'Eciton burchellii', ... "Atta colombica", ... 'Polyergus lucidus', ... 'Polyergus rufescens', ... ]).ask() ? Choose ant specie Atta colombica 'Atta colombica' .. image:: ../images/autocomplete.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 contains items as strings default: Default return value (single value). qmark: Question prefix displayed in front of the question. By default this is a ``?`` completer: A prompt_toolkit :class:`prompt_toolkit.completion.Completion` implementation. If not set, a questionary completer implementation will be used. meta_information: A dictionary with information/anything about choices. ignore_case: If true autocomplete would ignore case. match_middle: If true autocomplete would search in every string position not only in string begin. complete_style: How autocomplete menu would be shown, it could be ``COLUMN`` ``MULTI_COLUMN`` or ``READLINE_LIKE`` from :class:`prompt_toolkit.shortcuts.CompleteStyle`. 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 can either be a function accepting the input and returning a boolean, or an class reference to a subclass of the prompt toolkit Validator class. style: A custom color and style for the question parts. You can configure colors as well as font types for different elements. Returns: :class:`Question`: Question instance, ready to be prompted (using ``.ask()``). """ merged_style = merge_styles_default([style]) def get_prompt_tokens() -> List[Tuple[str, str]]: return [("class:qmark", qmark), ("class:question", " {} ".format(message))] def get_meta_style(meta: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]: if meta: for key in meta: meta[key] = HTML("{}").format(meta[key]) return meta validator = build_validator(validate) if completer is None: if not choices: raise ValueError("No choices is given, you should use Text question.") # use the default completer completer = WordCompleter( choices, ignore_case=ignore_case, meta_information=get_meta_style(meta_information), match_middle=match_middle, ) p: PromptSession = PromptSession( get_prompt_tokens, lexer=SimpleLexer("class:answer"), style=merged_style, completer=completer, validator=validator, complete_style=complete_style, **kwargs, ) p.default_buffer.reset(Document(default)) return Question(p.app)