Initial commit: Tamigo CLI with Gitea Actions and global installation support
This commit is contained in:
@@ -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)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
@@ -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__),
|
||||
)
|
||||
)
|
||||
670
venv/lib/python3.12/site-packages/questionary/prompts/common.py
Normal file
670
venv/lib/python3.12/site-packages/questionary/prompts/common.py
Normal 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)
|
||||
133
venv/lib/python3.12/site-packages/questionary/prompts/confirm.py
Normal file
133
venv/lib/python3.12/site-packages/questionary/prompts/confirm.py
Normal 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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
243
venv/lib/python3.12/site-packages/questionary/prompts/path.py
Normal file
243
venv/lib/python3.12/site-packages/questionary/prompts/path.py
Normal 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)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
283
venv/lib/python3.12/site-packages/questionary/prompts/select.py
Normal file
283
venv/lib/python3.12/site-packages/questionary/prompts/select.py
Normal 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__),
|
||||
)
|
||||
)
|
||||
101
venv/lib/python3.12/site-packages/questionary/prompts/text.py
Normal file
101
venv/lib/python3.12/site-packages/questionary/prompts/text.py
Normal 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)
|
||||
Reference in New Issue
Block a user