Initial commit: Tamigo CLI with Gitea Actions and global installation support

This commit is contained in:
Daniel Dybing
2026-03-11 12:07:08 +01:00
commit 146b79660d
2675 changed files with 462625 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
from questionary.prompts import autocomplete
from questionary.prompts import checkbox
from questionary.prompts import confirm
from questionary.prompts import password
from questionary.prompts import path
from questionary.prompts import press_any_key_to_continue
from questionary.prompts import rawselect
from questionary.prompts import select
from questionary.prompts import text
AVAILABLE_PROMPTS = {
"autocomplete": autocomplete.autocomplete,
"confirm": confirm.confirm,
"text": text.text,
"select": select.select,
"rawselect": rawselect.rawselect,
"password": password.password,
"checkbox": checkbox.checkbox,
"path": path.path,
"press_any_key_to_continue": press_any_key_to_continue.press_any_key_to_continue,
# backwards compatible names
"list": select.select,
"rawlist": rawselect.rawselect,
"input": text.text,
}
def prompt_by_name(name):
return AVAILABLE_PROMPTS.get(name)

View File

@@ -0,0 +1,214 @@
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("{}<b><u>{}</u></b>{}").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 species',
... choices=[
... 'Camponotus pennsylvanicus',
... 'Linepithema humile',
... 'Eciton burchellii',
... "Atta colombica",
... 'Polyergus lucidus',
... 'Polyergus rufescens',
... ]).ask()
? Choose ant species 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("<text>{}</text>").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)

View File

@@ -0,0 +1,327 @@
import string
from typing import Any
from typing import Callable
from typing import Dict
from typing import List
from typing import Optional
from typing import Sequence
from typing import Tuple
from typing import Union
from prompt_toolkit.application import Application
from prompt_toolkit.formatted_text import FormattedText
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.constants import INVALID_INPUT
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 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,
use_emacs_keys: bool = True,
use_search_filter: Union[str, bool, None] = False,
instruction: Optional[str] = None,
show_description: 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.
use_emacs_keys: Allow the user to select items from the list using
`Ctrl+N` (down) and `Ctrl+P` (up) keys.
use_search_filter: Flag to enable search filtering. Typing some string will
filter the choices to keep only the ones that contain the
search string.
Note that activating this option disables "vi-like"
navigation as "j" and "k" can be part of a prefix and
therefore cannot be used for navigation
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()``).
"""
if not (use_arrow_keys or use_jk_keys or use_emacs_keys):
raise ValueError(
"Some option to move the selection is required. Arrow keys or j/k or "
"Emacs keys."
)
if use_jk_keys and use_search_filter:
raise ValueError(
"Cannot use j/k keys with prefix filter search, since j/k can be part of the prefix."
)
merged_style = merge_styles_default(
[
# 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,
show_description=show_description,
)
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:
if instruction is not None:
tokens.append(("class:instruction", instruction))
else:
tokens.append(
(
"class:instruction",
"(Use arrow keys to move, "
"<space> to select, "
f"<{'ctrl-a' if use_search_filter else 'a'}> to toggle, "
f"<{'ctrl-a' if use_search_filter else 'i'}> to invert"
f"{', type to filter' if use_search_filter else ''})",
)
)
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 # type: ignore[assignment]
)
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(Keys.ControlI if use_search_filter else "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(Keys.ControlA if use_search_filter else "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_search_filter:
def search_filter(event):
ic.add_search_character(event.key_sequence[0].key)
for character in string.printable:
if character in string.whitespace:
continue
bindings.add(character, eager=True)(search_filter)
bindings.add(Keys.Backspace, eager=True)(search_filter)
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):
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."""
return Question(
Application(
layout=layout,
key_bindings=bindings,
style=merged_style,
**utils.used_kwargs(kwargs, Application.__init__),
)
)

View File

@@ -0,0 +1,670 @@
import inspect
from typing import Any
from typing import Callable
from typing import Dict
from typing import List
from typing import Optional
from typing import Sequence
from typing import Tuple
from typing import Union
from prompt_toolkit import PromptSession
from prompt_toolkit.filters import Always
from prompt_toolkit.filters import Condition
from prompt_toolkit.filters import IsDone
from prompt_toolkit.keys import Keys
from prompt_toolkit.layout import ConditionalContainer
from prompt_toolkit.layout import FormattedTextControl
from prompt_toolkit.layout import HSplit
from prompt_toolkit.layout import Layout
from prompt_toolkit.layout import Window
from prompt_toolkit.layout.controls import BufferControl
from prompt_toolkit.layout.dimension import LayoutDimension
from prompt_toolkit.styles import Style
from prompt_toolkit.validation import ValidationError
from prompt_toolkit.validation import Validator
from questionary.constants import DEFAULT_SELECTED_POINTER
from questionary.constants import DEFAULT_STYLE
from questionary.constants import INDICATOR_SELECTED
from questionary.constants import INDICATOR_UNSELECTED
from questionary.constants import INVALID_INPUT
# This is a cut-down version of `prompt_toolkit.formatted_text.AnyFormattedText`
# which does not exist in v2 of prompt_toolkit
FormattedText = Union[
str,
List[Tuple[str, str]],
List[Tuple[str, str, Callable[[Any], None]]],
None,
]
class Choice:
"""One choice in a :meth:`select`, :meth:`rawselect` or :meth:`checkbox`.
Args:
title: Text shown in the selection list.
value: Value returned, when the choice is selected. If this argument
is `None` or unset, then the value of `title` is used.
disabled: If set, the choice can not be selected by the user. The
provided text is used to explain, why the selection is
disabled.
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
"""Display string for the choice"""
value: Optional[Any]
"""Value of the choice"""
disabled: Optional[str]
"""Whether the choice can be selected"""
checked: Optional[bool]
"""Whether the choice is initially selected"""
__shortcut_key: Optional[Union[str, bool]]
description: Optional[str]
"""Choice description"""
def __init__(
self,
title: FormattedText,
value: Optional[Any] = None,
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.shortcut_key = shortcut_key
# self.auto_shortcut is set by the self.shortcut_key setter
self.checked = checked if checked is not None else False
self.description = description
if value is not None:
self.value = value
elif isinstance(title, list):
self.value = "".join([token[1] for token in title])
else:
self.value = title
@staticmethod
def build(c: Union[str, "Choice", Dict[str, Any]]) -> "Choice":
"""Create a choice object from different representations.
Args:
c: Either a :obj:`str`, :class:`Choice` or :obj:`dict` with
``name``, ``value``, ``disabled``, ``checked`` and
``key`` properties.
Returns:
An instance of the :class:`Choice` object.
"""
if isinstance(c, Choice):
return c
elif isinstance(c, str):
return Choice(c, c)
else:
return Choice(
c.get("name"),
c.get("value"),
c.get("disabled", None),
c.get("checked"),
c.get("key"),
c.get("description", None),
)
@property
def shortcut_key(self) -> Optional[Union[str, bool]]:
"""A shortcut key for the choice"""
return self.__shortcut_key
@shortcut_key.setter
def shortcut_key(self, key: Optional[Union[str, bool]]):
if key is not None:
if isinstance(key, bool):
self.__auto_shortcut = key
self.__shortcut_key = None
else:
self.__shortcut_key = str(key)
self.__auto_shortcut = False
else:
self.__shortcut_key = None
self.__auto_shortcut = True
@shortcut_key.deleter
def shortcut_key(self):
self.__shortcut_key = None
self.__auto_shortcut = True
def get_shortcut_title(self):
if self.shortcut_key is None:
return "-) "
else:
return "{}) ".format(self.shortcut_key)
@property
def auto_shortcut(self) -> bool:
"""Whether to assign a shortcut key to the choice
Keys are assigned starting with numbers and proceeding
through the ASCII alphabet.
"""
return self.__auto_shortcut
@auto_shortcut.setter
def auto_shortcut(self, should_assign: bool):
self.__auto_shortcut = should_assign
if self.__auto_shortcut:
self.__shortcut_key = None
@auto_shortcut.deleter
def auto_shortcut(self):
self.__auto_shortcut = False
class Separator(Choice):
"""Used to space/separate choices group."""
default_separator: str = "-" * 15
"""The default separator used if none is specified"""
line: str
"""The string being used as a separator"""
def __init__(self, line: Optional[str] = None) -> None:
"""Create a separator in a list.
Args:
line: Text to be displayed in the list, by default uses ``---``.
"""
self.line = line or self.default_separator
super().__init__(self.line, None, "-")
class InquirerControl(FormattedTextControl):
SHORTCUT_KEYS = [
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"0",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
]
choices: List[Choice]
default: Optional[Union[str, Choice, Dict[str, Any]]]
selected_options: List[Any]
search_filter: Union[str, None] = None
use_indicator: bool
use_shortcuts: bool
use_arrow_keys: bool
pointer: Optional[str]
pointed_at: int
is_answered: bool
show_description: bool
def __init__(
self,
choices: Sequence[Union[str, Choice, Dict[str, Any]]],
default: Optional[Union[str, Choice, Dict[str, Any]]] = None,
pointer: Optional[str] = DEFAULT_SELECTED_POINTER,
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,
):
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
if isinstance(default, Choice):
default = default.value
choices_values = [
choice.value for choice in choices if isinstance(choice, Choice)
]
if (
default is not None
and default not in choices
and default not in choices_values
):
raise ValueError(
f"Invalid `default` value passed. The value (`{default}`) "
f"does not exist in the set of choices. Please make sure the "
f"default value is one of the available choices."
)
if initial_choice is None:
pointed_at = None
elif initial_choice in choices:
pointed_at = choices.index(initial_choice)
elif initial_choice in choices_values:
for k, choice in enumerate(choices):
if isinstance(choice, Choice):
if choice.value == initial_choice:
pointed_at = k
break
else:
raise ValueError(
f"Invalid `initial_choice` value passed. The value "
f"(`{initial_choice}`) does not exist in "
f"the set of choices. Please make sure the initial value is "
f"one of the available choices."
)
self.is_answered = False
self.choices = []
self.submission_attempted = False
self.error_message = None
self.selected_options = []
self.found_in_search = False
self._init_choices(choices, pointed_at)
self._assign_shortcut_keys()
super().__init__(self._get_choice_tokens, **kwargs)
if not self.is_selection_valid():
raise ValueError(
f"Invalid 'initial_choice' value ('{initial_choice}'). "
f"It must be a selectable value."
)
def _is_selected(self, choice: Choice):
if isinstance(self.default, Choice):
compare_default = self.default == choice
else:
compare_default = self.default == choice.value
return choice.checked or compare_default and self.default is not None
def _assign_shortcut_keys(self):
available_shortcuts = self.SHORTCUT_KEYS[:]
# first, make sure we do not double assign a shortcut
for c in self.choices:
if c.shortcut_key is not None:
if c.shortcut_key in available_shortcuts:
available_shortcuts.remove(c.shortcut_key)
else:
raise ValueError(
"Invalid shortcut '{}'"
"for choice '{}'. Shortcuts "
"should be single characters or numbers. "
"Make sure that all your shortcuts are "
"unique.".format(c.shortcut_key, c.title)
)
shortcut_idx = 0
for c in self.choices:
if c.auto_shortcut and not c.disabled:
c.shortcut_key = available_shortcuts[shortcut_idx]
shortcut_idx += 1
if shortcut_idx == len(available_shortcuts):
break # fail gracefully if we run out of shortcuts
def _init_choices(
self,
choices: Sequence[Union[str, Choice, Dict[str, Any]]],
pointed_at: Optional[int],
):
# helper to convert from question format to internal format
self.choices = []
if pointed_at is not None:
self.pointed_at = pointed_at
for i, c in enumerate(choices):
choice = Choice.build(c)
if self._is_selected(choice):
self.selected_options.append(choice.value)
if pointed_at is None and not choice.disabled:
# find the first (available) choice
self.pointed_at = pointed_at = i
self.choices.append(choice)
@property
def filtered_choices(self):
if not self.search_filter:
return self.choices
filtered = [
c for c in self.choices if self.search_filter.lower() in c.title.lower()
]
self.found_in_search = len(filtered) > 0
return filtered if self.found_in_search else self.choices
@property
def choice_count(self) -> int:
return len(self.filtered_choices)
def _get_choice_tokens(self):
tokens = []
def append(index: int, choice: Choice):
# use value to check if option has been selected
selected = choice.value in self.selected_options
if index == self.pointed_at:
if self.pointer is not None:
tokens.append(("class:pointer", " {} ".format(self.pointer)))
else:
tokens.append(("class:text", " " * 3))
tokens.append(("[SetCursorPosition]", ""))
else:
pointer_length = len(self.pointer) if self.pointer is not None else 1
tokens.append(("class:text", " " * (2 + pointer_length)))
if isinstance(choice, Separator):
tokens.append(("class:separator", "{}".format(choice.title)))
elif choice.disabled: # disabled
if isinstance(choice.title, list):
tokens.append(
("class:selected" if selected else "class:disabled", "- ")
)
tokens.extend(choice.title)
else:
tokens.append(
(
"class:selected" if selected else "class:disabled",
"- {}".format(choice.title),
)
)
tokens.append(
(
"class:selected" if selected else "class:disabled",
"{}".format(
""
if isinstance(choice.disabled, bool)
else " ({})".format(choice.disabled)
),
)
)
else:
shortcut = choice.get_shortcut_title() if self.use_shortcuts else ""
if selected:
if self.use_indicator:
indicator = INDICATOR_SELECTED + " "
else:
indicator = ""
tokens.append(("class:selected", "{}".format(indicator)))
else:
if self.use_indicator:
indicator = INDICATOR_UNSELECTED + " "
else:
indicator = ""
tokens.append(("class:text", "{}".format(indicator)))
if isinstance(choice.title, list):
tokens.extend(choice.title)
elif selected:
tokens.append(
("class:selected", "{}{}".format(shortcut, choice.title))
)
elif index == self.pointed_at:
tokens.append(
("class:highlighted", "{}{}".format(shortcut, choice.title))
)
else:
tokens.append(("class:text", "{}{}".format(shortcut, choice.title)))
tokens.append(("", "\n"))
# prepare the select choices
for i, c in enumerate(self.filtered_choices):
append(i, c)
current = self.get_pointed_at()
if self.show_selected:
answer = current.get_shortcut_title() if self.use_shortcuts else ""
answer += (
current.title if isinstance(current.title, str) else current.title[0][1]
)
tokens.append(("class:text", " Answer: {}".format(answer)))
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:
selected = self.choices[self.pointed_at]
return isinstance(selected, Separator)
def is_selection_disabled(self) -> Optional[str]:
return self.choices[self.pointed_at].disabled
def is_selection_valid(self) -> bool:
return not self.is_selection_disabled() and not self.is_selection_a_separator()
def select_previous(self) -> None:
self.pointed_at = (self.pointed_at - 1) % self.choice_count
def select_next(self) -> None:
self.pointed_at = (self.pointed_at + 1) % self.choice_count
def get_pointed_at(self) -> Choice:
return self.filtered_choices[self.pointed_at]
def get_selected_values(self) -> List[Choice]:
# get values not labels
return [
c
for c in self.choices
if (not isinstance(c, Separator) and c.value in self.selected_options)
]
def add_search_character(self, char: Keys) -> None:
"""Adds a character to the search filter"""
if char == Keys.Backspace:
self.remove_search_character()
else:
if self.search_filter is None:
self.search_filter = str(char)
else:
self.search_filter += str(char)
# Make sure that the selection is in the bounds of the filtered list
self.pointed_at = 0
def remove_search_character(self) -> None:
if self.search_filter and len(self.search_filter) > 1:
self.search_filter = self.search_filter[:-1]
else:
self.search_filter = None
def get_search_string_tokens(self):
if self.search_filter is None:
return None
return [
("", "\n"),
("class:question-mark", "/ "),
(
"class:search_success" if self.found_in_search else "class:search_none",
self.search_filter,
),
("class:question-mark", "..."),
]
def build_validator(validate: Any) -> Optional[Validator]:
if validate:
if inspect.isclass(validate) and issubclass(validate, Validator):
return validate()
elif isinstance(validate, Validator):
return validate
elif callable(validate):
class _InputValidator(Validator):
def validate(self, document):
verdict = validate(document.text)
if verdict is not True:
if verdict is False:
verdict = INVALID_INPUT
raise ValidationError(
message=verdict, cursor_position=len(document.text)
)
return _InputValidator()
return None
def _fix_unecessary_blank_lines(ps: PromptSession) -> None:
"""This is a fix for additional empty lines added by prompt toolkit.
This assumes the layout of the default session doesn't change, if it
does, this needs an update."""
default_buffer_window: Window = next(
win
for win in ps.layout.find_all_windows()
if isinstance(win.content, BufferControl)
and win.content.buffer.name == "DEFAULT_BUFFER"
)
# this forces the main window to stay as small as possible, avoiding
# empty lines in selections
default_buffer_window.dont_extend_height = Always()
default_buffer_window.always_hide_cursor = Always()
def create_inquirer_layout(
ic: InquirerControl,
get_prompt_tokens: Callable[[], List[Tuple[str, str]]],
**kwargs: Any,
) -> Layout:
"""Create a layout combining question and inquirer selection."""
ps: PromptSession = PromptSession(
get_prompt_tokens, reserve_space_for_menu=0, **kwargs
)
_fix_unecessary_blank_lines(ps)
@Condition
def has_search_string():
return ic.get_search_string_tokens() is not None
validation_prompt: PromptSession = PromptSession(
bottom_toolbar=lambda: ic.error_message, **kwargs
)
return Layout(
HSplit(
[
ps.layout.container,
ConditionalContainer(Window(ic), filter=~IsDone()),
ConditionalContainer(
Window(
height=LayoutDimension.exact(2),
content=FormattedTextControl(ic.get_search_string_tokens),
),
filter=has_search_string & ~IsDone(),
),
ConditionalContainer(
validation_prompt.layout.container,
filter=Condition(lambda: ic.error_message is not None),
),
]
)
)
def print_formatted_text(text: str, style: Optional[str] = None, **kwargs: Any) -> None:
"""Print formatted text.
Sometimes you want to spice up your printed messages a bit,
:meth:`questionary.print` is a helper to do just that.
Example:
>>> import questionary
>>> questionary.print("Hello World 🦄", style="bold italic fg:darkred")
Hello World 🦄
.. image:: ../images/print.gif
Args:
text: Text to be printed.
style: Style used for printing. The style argument uses the
prompt :ref:`toolkit style strings <prompt_toolkit:styling>`.
"""
from prompt_toolkit import print_formatted_text as pt_print
from prompt_toolkit.formatted_text import FormattedText as FText
if style is not None:
text_style = Style([("text", style)])
else:
text_style = DEFAULT_STYLE
pt_print(FText([("class:text", text)]), style=text_style, **kwargs)

View File

@@ -0,0 +1,133 @@
from typing import Any
from typing import Optional
from prompt_toolkit import PromptSession
from prompt_toolkit.formatted_text import to_formatted_text
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.keys import Keys
from prompt_toolkit.styles import Style
from questionary.constants import DEFAULT_QUESTION_PREFIX
from questionary.constants import NO
from questionary.constants import NO_OR_YES
from questionary.constants import YES
from questionary.constants import YES_OR_NO
from questionary.question import Question
from questionary.styles import merge_styles_default
def confirm(
message: str,
default: bool = True,
qmark: str = DEFAULT_QUESTION_PREFIX,
style: Optional[Style] = None,
auto_enter: bool = True,
instruction: Optional[str] = None,
**kwargs: Any,
) -> Question:
"""A yes or no question. The user can either confirm or deny.
This question type can be used to prompt the user for a confirmation
of a yes-or-no question. If the user just hits enter, the default
value will be returned.
Example:
>>> import questionary
>>> questionary.confirm("Are you amazed?").ask()
? Are you amazed? Yes
True
.. image:: ../images/confirm.gif
This is just a really basic example, the prompt can be customised using the
parameters.
Args:
message: Question text.
default: Default value will be returned if the user just hits
enter.
qmark: Question prefix displayed in front of the question.
By default this is a ``?``.
style: A custom color and style for the question parts. You can
configure colors as well as font types for different elements.
auto_enter: If set to `False`, the user needs to press the 'enter' key to
accept their answer. If set to `True`, a valid input will be
accepted without the need to press 'Enter'.
instruction: A message describing how to proceed through the
confirmation prompt.
Returns:
:class:`Question`: Question instance, ready to be prompted (using `.ask()`).
"""
merged_style = merge_styles_default([style])
status = {"answer": None, "complete": False}
def get_prompt_tokens():
tokens = []
tokens.append(("class:qmark", qmark))
tokens.append(("class:question", " {} ".format(message)))
if instruction is not None:
tokens.append(("class:instruction", instruction))
elif not status["complete"]:
_instruction = YES_OR_NO if default else NO_OR_YES
tokens.append(("class:instruction", "{} ".format(_instruction)))
if status["answer"] is not None:
answer = YES if status["answer"] else NO
tokens.append(("class:answer", answer))
return to_formatted_text(tokens)
def exit_with_result(event):
status["complete"] = True
event.app.exit(result=status["answer"])
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("n")
@bindings.add("N")
def key_n(event):
status["answer"] = False
if auto_enter:
exit_with_result(event)
@bindings.add("y")
@bindings.add("Y")
def key_y(event):
status["answer"] = True
if auto_enter:
exit_with_result(event)
@bindings.add(Keys.ControlH)
def key_backspace(event):
status["answer"] = None
@bindings.add(Keys.ControlM, eager=True)
def set_answer(event):
if status["answer"] is None:
status["answer"] = default
exit_with_result(event)
@bindings.add(Keys.Any)
def other(event):
"""Disallow inserting other text."""
return Question(
PromptSession(
get_prompt_tokens, key_bindings=bindings, style=merged_style, **kwargs
).app
)

View File

@@ -0,0 +1,61 @@
from typing import Any
from typing import Optional
from questionary import Style
from questionary.constants import DEFAULT_QUESTION_PREFIX
from questionary.prompts import text
from questionary.question import Question
def password(
message: str,
default: str = "",
validate: Any = None,
qmark: str = DEFAULT_QUESTION_PREFIX,
style: Optional[Style] = None,
**kwargs: Any,
) -> Question:
"""A text input where a user can enter a secret which won't be displayed on the CLI.
This question type can be used to prompt the user for information
that should not be shown in the command line. The typed text will be
replaced with ``*``.
Example:
>>> import questionary
>>> questionary.password("What's your secret?").ask()
? What's your secret? ********
'secret42'
.. image:: ../images/password.gif
This is just a really basic example, the prompt can be customised using the
parameters.
Args:
message: Question text.
default: Default value will be returned if the user just hits
enter.
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.
qmark: Question prefix displayed in front of the question.
By default this is a ``?``.
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()``).
"""
return text.text(
message, default, validate, qmark, style, is_password=True, **kwargs
)

View File

@@ -0,0 +1,243 @@
import os
from typing import Any
from typing import Callable
from typing import Iterable
from typing import List
from typing import Optional
from typing import Tuple
from prompt_toolkit.completion import CompleteEvent
from prompt_toolkit.completion import Completion
from prompt_toolkit.completion import PathCompleter
from prompt_toolkit.completion.base import Completer
from prompt_toolkit.document import Document
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
from prompt_toolkit.keys import Keys
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 GreatUXPathCompleter(PathCompleter):
"""Wraps :class:`prompt_toolkit.completion.PathCompleter`.
Makes sure completions for directories end with a path separator. Also make sure
the right path separator is used. Checks if `get_paths` returns list of existing
directories.
"""
def __init__(
self,
only_directories: bool = False,
get_paths: Optional[Callable[[], List[str]]] = None,
file_filter: Optional[Callable[[str], bool]] = None,
min_input_len: int = 0,
expanduser: bool = False,
) -> None:
"""Adds validation of 'get_paths' to :class:`prompt_toolkit.completion.PathCompleter`.
Args:
only_directories (bool): If True, only directories will be
returned, but no files. Defaults to False.
get_paths (Callable[[], List[str]], optional): Callable which
returns a list of directories to look into when the user enters a
relative path. If None, set to (lambda: ["."]). Defaults to None.
file_filter (Callable[[str], bool], optional): Callable which
takes a filename and returns whether this file should show up in the
completion. ``None`` when no filtering has to be done. Defaults to None.
min_input_len (int): Don't do autocompletion when the input string
is shorter. Defaults to 0.
expanduser (bool): If True, tilde (~) is expanded. Defaults to
False.
Raises:
ValueError: If any of the by `get_paths` returned directories does not
exist.
"""
# if get_paths is None, make it return the current working dir
get_paths = get_paths or (lambda: ["."])
# validation of get_paths
for current_path in get_paths():
if not os.path.isdir(current_path):
raise (
ValueError(
"\n Completer for file paths 'get_paths' must return only existing directories, but"
f" '{current_path}' does not exist."
)
)
# call PathCompleter __init__
super().__init__(
only_directories=only_directories,
get_paths=get_paths,
file_filter=file_filter,
min_input_len=min_input_len,
expanduser=expanduser,
)
def get_completions(
self, document: Document, complete_event: CompleteEvent
) -> Iterable[Completion]:
"""Get completions.
Wraps :class:`prompt_toolkit.completion.PathCompleter`. Makes sure completions
for directories end with a path separator. Also make sure the right path
separator is used.
"""
completions = super(GreatUXPathCompleter, self).get_completions(
document, complete_event
)
for completion in completions:
# check if the display value ends with a path separator.
# first check if display is properly set
styled_display = completion.display[0]
# styled display is a formatted text (a tuple of the text and its style)
# second tuple entry is the text
if styled_display[1][-1] == "/":
# replace separator with the OS specific one
display_text = styled_display[1][:-1] + os.path.sep
# update the styled display with the modified text
completion.display[0] = (styled_display[0], display_text)
# append the separator to the text as well - unclear why the normal
# path completer omits it from the text. this improves UX for the
# user, as they don't need to type the separator after auto-completing
# a directory
completion.text += os.path.sep
yield completion
def path(
message: str,
default: str = "",
qmark: str = DEFAULT_QUESTION_PREFIX,
validate: Any = None,
completer: Optional[Completer] = None,
style: Optional[Style] = None,
only_directories: bool = False,
get_paths: Optional[Callable[[], List[str]]] = None,
file_filter: Optional[Callable[[str], bool]] = None,
complete_style: CompleteStyle = CompleteStyle.MULTI_COLUMN,
**kwargs: Any,
) -> Question:
"""A text input for a file or directory path with autocompletion enabled.
Example:
>>> import questionary
>>> questionary.path(
>>> "What's the path to the projects version file?"
>>> ).ask()
? What's the path to the projects version file? ./pyproject.toml
'./pyproject.toml'
.. image:: ../images/path.gif
This is just a really basic example, the prompt can be customized using the
parameters.
Args:
message: Question text.
default: Default return value (single value).
qmark: Question prefix displayed in front of the question.
By default this is a ``?``.
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.
completer: A custom completer to use in the prompt. For more information,
see `this <https://python-prompt-toolkit.readthedocs.io/en/master/pages/asking_for_input.html#a-custom-completer>`_.
style: A custom color and style for the question parts. You can
configure colors as well as font types for different elements.
only_directories: Only show directories in auto completion. This option
does not do anything if a custom ``completer`` is
passed.
get_paths: Set a callable to generate paths to traverse for suggestions. This option
does not do anything if a custom ``completer`` is
passed.
file_filter: Optional callable to filter suggested paths. Only paths
where the passed callable evaluates to ``True`` will show up in
the suggested paths. This does not validate the typed path, e.g.
it is still possible for the user to enter a path manually, even
though this filter evaluates to ``False``. If in addition to
filtering suggestions you also want to validate the result, use
``validate`` in combination with the ``file_filter``.
Returns:
:class:`Question`: Question instance, ready to be prompted (using ``.ask()``).
""" # noqa: W505, E501
merged_style = merge_styles_default([style])
def get_prompt_tokens() -> List[Tuple[str, str]]:
return [("class:qmark", qmark), ("class:question", " {} ".format(message))]
validator = build_validator(validate)
completer = completer or GreatUXPathCompleter(
get_paths=get_paths,
only_directories=only_directories,
file_filter=file_filter,
expanduser=True,
)
bindings = KeyBindings()
@bindings.add(Keys.ControlM, eager=True)
def set_answer(event: KeyPressEvent):
if event.current_buffer.complete_state is not None:
event.current_buffer.complete_state = None
elif event.app.current_buffer.validate(set_cursor=True):
# When the validation succeeded, accept the input.
result_path = event.app.current_buffer.document.text
if result_path.endswith(os.path.sep):
result_path = result_path[:-1]
event.app.exit(result=result_path)
event.app.current_buffer.append_to_history()
@bindings.add(os.path.sep, eager=True)
def next_segment(event: KeyPressEvent):
b = event.app.current_buffer
if b.complete_state:
b.complete_state = None
current_path = b.document.text
if not current_path.endswith(os.path.sep):
b.insert_text(os.path.sep)
b.start_completion(select_first=False)
p: PromptSession = PromptSession(
get_prompt_tokens,
lexer=SimpleLexer("class:answer"),
style=merged_style,
completer=completer,
validator=validator,
complete_style=complete_style,
key_bindings=bindings,
**kwargs,
)
p.default_buffer.reset(Document(default))
return Question(p.app)

View File

@@ -0,0 +1,61 @@
from typing import Any
from typing import Optional
from prompt_toolkit import PromptSession
from prompt_toolkit.formatted_text import to_formatted_text
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.keys import Keys
from prompt_toolkit.styles import Style
from questionary.question import Question
from questionary.styles import merge_styles_default
def press_any_key_to_continue(
message: Optional[str] = None,
style: Optional[Style] = None,
**kwargs: Any,
):
"""Wait until user presses any key to continue.
Example:
>>> import questionary
>>> questionary.press_any_key_to_continue().ask()
Press any key to continue...
''
Args:
message: Question text. Defaults to ``"Press any key to continue..."``
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])
if message is None:
message = "Press any key to continue..."
def get_prompt_tokens():
tokens = []
tokens.append(("class:question", f" {message} "))
return to_formatted_text(tokens)
def exit_with_result(event):
event.app.exit(result=None)
bindings = KeyBindings()
@bindings.add(Keys.Any)
def any_key(event):
exit_with_result(event)
return Question(
PromptSession(
get_prompt_tokens, key_bindings=bindings, style=merged_style, **kwargs
).app
)

View File

@@ -0,0 +1,79 @@
from typing import Any
from typing import Dict
from typing import Optional
from typing import Sequence
from typing import Union
from prompt_toolkit.styles import Style
from questionary.constants import DEFAULT_QUESTION_PREFIX
from questionary.constants import DEFAULT_SELECTED_POINTER
from questionary.prompts import select
from questionary.prompts.common import Choice
from questionary.question import Question
def rawselect(
message: str,
choices: Sequence[Union[str, Choice, Dict[str, Any]]],
default: Optional[str] = None,
qmark: str = DEFAULT_QUESTION_PREFIX,
pointer: Optional[str] = DEFAULT_SELECTED_POINTER,
style: Optional[Style] = None,
**kwargs: Any,
) -> Question:
"""Ask the user to select one item from a list of choices using shortcuts.
The user can only select one option.
Example:
>>> import questionary
>>> questionary.rawselect(
... "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/rawselect.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).
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.
Returns:
:class:`Question`: Question instance, ready to be prompted (using ``.ask()``).
"""
return select.select(
message,
choices,
default,
qmark,
pointer,
style,
use_shortcuts=True,
use_arrow_keys=False,
**kwargs,
)

View File

@@ -0,0 +1,283 @@
# -*- coding: utf-8 -*-
import string
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,
use_search_filter: bool = False,
show_selected: bool = False,
show_description: bool = True,
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.
use_search_filter: Flag to enable search filtering. Typing some string will
filter the choices to keep only the ones that contain the
search string.
Note that activating this option disables "vi-like"
navigation as "j" and "k" can be part of a prefix and
therefore cannot be used for navigation
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()``).
"""
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_jk_keys and use_search_filter:
raise ValueError(
"Cannot use j/k keys with prefix filter search, since j/k can be part of the prefix."
)
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:
real_len_of_choices = sum(1 for c in choices if not isinstance(c, Separator))
if real_len_of_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), real_len_of_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,
show_description=show_description,
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 = f"(Use shortcuts or arrow keys{', type to filter' if use_search_filter else ''})"
elif use_shortcuts and not use_arrow_keys:
instruction_msg = f"(Use shortcuts{', type to filter' if use_search_filter else ''})"
else:
instruction_msg = f"(Use arrow keys{', type to filter' if use_search_filter else ''})"
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 or c.disabled:
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_search_filter:
def search_filter(event):
ic.add_search_character(event.key_sequence[0].key)
for character in string.printable:
bindings.add(character, eager=True)(search_filter)
bindings.add(Keys.Backspace, eager=True)(search_filter)
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__),
)
)

View File

@@ -0,0 +1,101 @@
from typing import Any
from typing import List
from typing import Optional
from typing import Tuple
from prompt_toolkit.document import Document
from prompt_toolkit.lexers import Lexer
from prompt_toolkit.lexers import SimpleLexer
from prompt_toolkit.shortcuts.prompt import PromptSession
from prompt_toolkit.styles import Style
from questionary.constants import DEFAULT_QUESTION_PREFIX
from questionary.constants import INSTRUCTION_MULTILINE
from questionary.prompts.common import build_validator
from questionary.question import Question
from questionary.styles import merge_styles_default
def text(
message: str,
default: str = "",
validate: Any = None,
qmark: str = DEFAULT_QUESTION_PREFIX,
style: Optional[Style] = None,
multiline: bool = False,
instruction: Optional[str] = None,
lexer: Optional[Lexer] = None,
**kwargs: Any,
) -> Question:
"""Prompt the user to enter a free text message.
This question type can be used to prompt the user for some text input.
Example:
>>> import questionary
>>> questionary.text("What's your first name?").ask()
? What's your first name? Tom
'Tom'
.. image:: ../images/text.gif
This is just a really basic example, the prompt can be customised using the
parameters.
Args:
message: Question text.
default: Default value will be returned if the user just hits
enter.
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.
qmark: Question prefix displayed in front of the question.
By default this is a ``?``.
style: A custom color and style for the question parts. You can
configure colors as well as font types for different elements.
multiline: If ``True``, multiline input will be enabled.
instruction: Write instructions for the user if needed. If ``None``
and ``multiline=True``, some instructions will appear.
lexer: Supply a valid lexer to style the answer. Leave empty to
use a simple one by default.
kwargs: Additional arguments, they will be passed to prompt toolkit.
Returns:
:class:`Question`: Question instance, ready to be prompted (using ``.ask()``).
"""
merged_style = merge_styles_default([style])
lexer = lexer or SimpleLexer("class:answer")
validator = build_validator(validate)
if instruction is None and multiline:
instruction = INSTRUCTION_MULTILINE
def get_prompt_tokens() -> List[Tuple[str, str]]:
result = [("class:qmark", qmark), ("class:question", " {} ".format(message))]
if instruction:
result.append(("class:instruction", " {} ".format(instruction)))
return result
p: PromptSession = PromptSession(
get_prompt_tokens,
style=merged_style,
validator=validator,
lexer=lexer,
multiline=multiline,
**kwargs,
)
p.default_buffer.reset(Document(default))
return Question(p.app)