|
"""Module contains shared utility functions and typing aliases.""" |
|
import math |
|
import os |
|
import shutil |
|
from typing import ( |
|
TYPE_CHECKING, |
|
Any, |
|
Callable, |
|
Dict, |
|
List, |
|
NamedTuple, |
|
Optional, |
|
Tuple, |
|
Union, |
|
) |
|
|
|
from prompt_toolkit import print_formatted_text |
|
from prompt_toolkit.application import run_in_terminal |
|
from prompt_toolkit.application.current import get_app |
|
from prompt_toolkit.formatted_text import FormattedText |
|
from prompt_toolkit.styles import Style |
|
from prompt_toolkit.validation import Validator |
|
|
|
from InquirerPy.exceptions import InvalidArgument |
|
|
|
if TYPE_CHECKING: |
|
from prompt_toolkit.filters.base import FilterOrBool |
|
|
|
from InquirerPy.base.control import Choice |
|
|
|
__all__ = [ |
|
"get_style", |
|
"calculate_height", |
|
"InquirerPyStyle", |
|
"patched_print", |
|
"color_print", |
|
] |
|
|
|
|
|
class InquirerPyStyle(NamedTuple): |
|
"""`InquirerPy` Style class. |
|
|
|
Used as a helper class to enforce the method `get_style` to be used |
|
while also avoiding :class:`dict` to be passed into prompts. |
|
|
|
Note: |
|
The class is an instance of :class:`typing.NamedTuple`. |
|
|
|
Warning: |
|
You should not directly be using this class besides for type hinting |
|
purposes. Obtain an instance of this class using :func:`.get_style`. |
|
""" |
|
|
|
dict: Dict[str, str] |
|
|
|
|
|
InquirerPySessionResult = Dict[Union[str, int], Optional[Union[str, bool, List[Any]]]] |
|
InquirerPyChoice = Union[List[Any], List["Choice"], List[Dict[str, Any]]] |
|
InquirerPyListChoices = Union[ |
|
Callable[["InquirerPySessionResult"], InquirerPyChoice], |
|
InquirerPyChoice, |
|
] |
|
InquirerPyValidate = Union[Callable[[Any], bool], "Validator"] |
|
InquirerPyQuestions = Union[List[Dict[str, Any]], Dict[str, Any]] |
|
InquirerPyMessage = Union[str, Callable[["InquirerPySessionResult"], str]] |
|
InquirerPyDefault = Union[Any, Callable[["InquirerPySessionResult"], Any]] |
|
InquirerPyKeybindings = Dict[ |
|
str, List[Dict[str, Union[str, "FilterOrBool", List[str]]]] |
|
] |
|
|
|
|
|
def get_style( |
|
style: Optional[Dict[str, str]] = None, style_override: bool = True |
|
) -> InquirerPyStyle: |
|
"""Obtain an :class:`.InquirerPyStyle` instance which can be consumed by the `style` parameter in prompts. |
|
|
|
Tip: |
|
This function supports ENV variables. |
|
|
|
For all the color ENV variable names, refer to the :ref:`ENV <pages/env:Style>` documentation. |
|
|
|
Note: |
|
If no style is provided, then a default theme based on `one dark <https://github.com/joshdick/onedark.vim#color-reference>`_ |
|
color palette is applied. |
|
|
|
Note: |
|
Priority: style parameter -> ENV variable -> default style |
|
|
|
Args: |
|
style: The dictionary of style classes and their colors, If nothing is passed, the style will be resolved to the :ref:`pages/style:Default Style`. |
|
style_override: A boolean to determine if the supplied `style` parameter should be merged with the :ref:`pages/style:Default Style` or override them. |
|
By default, the supplied style will overwrite the :ref:`pages/style:Default Style`. |
|
|
|
Returns: |
|
An instance of :class:`.InquirerPyStyle`. |
|
|
|
Examples: |
|
>>> from InquirerPy import get_style |
|
>>> from InquirerPy import inquirer |
|
>>> style = get_style({"questionmark": "#ffffff", "answer": "#000000"}, style_override=False) |
|
>>> result = inquirer.confirm(message="Confirm?", style=style).execute() |
|
""" |
|
if not style_override or style is None: |
|
if not style: |
|
style = {} |
|
result = { |
|
"questionmark": os.getenv("INQUIRERPY_STYLE_QUESTIONMARK", "#e5c07b"), |
|
"answermark": os.getenv("INQUIRERPY_STYLE_ANSWERMARK", "#e5c07b"), |
|
"answer": os.getenv("INQUIRERPY_STYLE_ANSWER", "#61afef"), |
|
"input": os.getenv("INQUIRERPY_STYLE_INPUT", "#98c379"), |
|
"question": os.getenv("INQUIRERPY_STYLE_QUESTION", ""), |
|
"answered_question": os.getenv("INQUIRERPY_STYLE_ANSWERED_QUESTION", ""), |
|
"instruction": os.getenv("INQUIRERPY_STYLE_INSTRUCTION", "#abb2bf"), |
|
"long_instruction": os.getenv( |
|
"INQUIRERPY_STYLE_LONG_INSTRUCTION", "#abb2bf" |
|
), |
|
"pointer": os.getenv("INQUIRERPY_STYLE_POINTER", "#61afef"), |
|
"checkbox": os.getenv("INQUIRERPY_STYLE_CHECKBOX", "#98c379"), |
|
"separator": os.getenv("INQUIRERPY_STYLE_SEPARATOR", ""), |
|
"skipped": os.getenv("INQUIRERPY_STYLE_SKIPPED", "#5c6370"), |
|
"validator": os.getenv("INQUIRERPY_STYLE_VALIDATOR", ""), |
|
"marker": os.getenv("INQUIRERPY_STYLE_MARKER", "#e5c07b"), |
|
"fuzzy_prompt": os.getenv("INQUIRERPY_STYLE_FUZZY_PROMPT", "#c678dd"), |
|
"fuzzy_info": os.getenv("INQUIRERPY_STYLE_FUZZY_INFO", "#abb2bf"), |
|
"fuzzy_border": os.getenv("INQUIRERPY_STYLE_FUZZY_BORDER", "#4b5263"), |
|
"fuzzy_match": os.getenv("INQUIRERPY_STYLE_FUZZY_MATCH", "#c678dd"), |
|
"spinner_pattern": os.getenv("INQUIRERPY_STYLE_SPINNER_PATTERN", "#e5c07b"), |
|
"spinner_text": os.getenv("INQUIRERPY_STYLE_SPINNER_TEXT", ""), |
|
**style, |
|
} |
|
else: |
|
result = { |
|
"questionmark": os.getenv("INQUIRERPY_STYLE_QUESTIONMARK", ""), |
|
"answermark": os.getenv("INQUIRERPY_STYLE_ANSWERMARK", ""), |
|
"answer": os.getenv("INQUIRERPY_STYLE_ANSWER", ""), |
|
"input": os.getenv("INQUIRERPY_STYLE_INPUT", ""), |
|
"question": os.getenv("INQUIRERPY_STYLE_QUESTION", ""), |
|
"answered_question": os.getenv("INQUIRERPY_STYLE_ANSWERED_QUESTION", ""), |
|
"instruction": os.getenv("INQUIRERPY_STYLE_INSTRUCTION", ""), |
|
"long_instruction": os.getenv("INQUIRERPY_STYLE_LONG_INSTRUCTION", ""), |
|
"pointer": os.getenv("INQUIRERPY_STYLE_POINTER", ""), |
|
"checkbox": os.getenv("INQUIRERPY_STYLE_CHECKBOX", ""), |
|
"separator": os.getenv("INQUIRERPY_STYLE_SEPARATOR", ""), |
|
"skipped": os.getenv("INQUIRERPY_STYLE_SKIPPED", ""), |
|
"validator": os.getenv("INQUIRERPY_STYLE_VALIDATOR", ""), |
|
"marker": os.getenv("INQUIRERPY_STYLE_MARKER", ""), |
|
"fuzzy_prompt": os.getenv("INQUIRERPY_STYLE_FUZZY_PROMPT", ""), |
|
"fuzzy_info": os.getenv("INQUIRERPY_STYLE_FUZZY_INFO", ""), |
|
"fuzzy_border": os.getenv("INQUIRERPY_STYLE_FUZZY_BORDER", ""), |
|
"fuzzy_match": os.getenv("INQUIRERPY_STYLE_FUZZY_MATCH", ""), |
|
"spinner_pattern": os.getenv("INQUIRERPY_STYLE_SPINNER_PATTERN", ""), |
|
"spinner_text": os.getenv("INQUIRERPY_STYLE_SPINNER_TEXT", ""), |
|
**style, |
|
} |
|
|
|
if result.get("fuzzy_border"): |
|
result["frame.border"] = result.pop("fuzzy_border") |
|
if result.get("validator"): |
|
result["validation-toolbar"] = result.pop("validator") |
|
result["bottom-toolbar"] = "noreverse" |
|
return InquirerPyStyle(result) |
|
|
|
|
|
def calculate_height( |
|
height: Optional[Union[int, str]], |
|
max_height: Optional[Union[int, str]], |
|
height_offset: int = 2, |
|
) -> Tuple[Optional[int], int]: |
|
"""Calculate the `height` and `max_height` for the main question contents. |
|
|
|
Tip: |
|
The parameter `height`/`max_height` can be specified by either a :class:`string` or :class:`int`. |
|
|
|
When `height`/`max_height` is :class:`str`: |
|
It will set the height to a percentage based on the value provided. |
|
You can optionally add the '%' sign which will be ignored while processing. |
|
|
|
Example: "60%" or "60" (60% of the current terminal visible lines) |
|
|
|
When `height`/`max_height` is :class:`int`: |
|
It will set the height to exact number of lines based on the value provided. |
|
|
|
Example: 20 (20 lines in terminal) |
|
|
|
Note: |
|
If `max_height` is not provided or is None, the default `max_height` will be configured to `70%` for |
|
best visual presentation in the terminal. |
|
|
|
Args: |
|
height: The desired height in either percentage as string or exact value as int. |
|
max_height: Maximum acceptable height in either percentage as string or exact value as int. |
|
height_offset: Height offset to apply to the height. |
|
|
|
Returns: |
|
A :class:`tuple` with the first value being the desired height and the second value being |
|
the maximum height. |
|
|
|
Raises: |
|
InvalidArgument: The provided `height`/`max_height` is not able to to be converted to int. |
|
|
|
Examples: |
|
>>> calculate_height(height="60%", max_height="100%") |
|
""" |
|
try: |
|
_, term_lines = shutil.get_terminal_size() |
|
term_lines = term_lines |
|
if not height: |
|
dimmension_height = None |
|
else: |
|
if isinstance(height, str): |
|
height = height.replace("%", "") |
|
height = int(height) |
|
dimmension_height = ( |
|
math.floor(term_lines * (height / 100)) - height_offset |
|
) |
|
else: |
|
dimmension_height = height |
|
|
|
if not max_height: |
|
max_height = "70%" if not height else "100%" |
|
if isinstance(max_height, str): |
|
max_height = max_height.replace("%", "") |
|
max_height = int(max_height) |
|
dimmension_max_height = ( |
|
math.floor(term_lines * (max_height / 100)) - height_offset |
|
) |
|
else: |
|
dimmension_max_height = max_height |
|
|
|
if dimmension_height and dimmension_height > dimmension_max_height: |
|
dimmension_height = dimmension_max_height |
|
if dimmension_height and dimmension_height <= 0: |
|
dimmension_height = 1 |
|
if dimmension_max_height <= 0: |
|
dimmension_max_height = 1 |
|
return dimmension_height, dimmension_max_height |
|
|
|
except ValueError: |
|
raise InvalidArgument( |
|
"prompt argument height/max_height needs to be type of an int or str" |
|
) |
|
|
|
|
|
def patched_print(*values) -> None: |
|
"""Patched :func:`print` that can print values without interrupting the prompt. |
|
|
|
See Also: |
|
:func:`print` |
|
:func:`~prompt_toolkit.application.run_in_terminal` |
|
|
|
Args: |
|
*values: Refer to :func:`print`. |
|
|
|
Examples: |
|
>>> patched_print("Hello World") |
|
""" |
|
|
|
def _print(): |
|
print(*values) |
|
|
|
run_in_terminal(_print) |
|
|
|
|
|
def color_print( |
|
formatted_text: List[Tuple[str, str]], style: Optional[Dict[str, str]] = None |
|
) -> None: |
|
"""Print colored text leveraging :func:`~prompt_toolkit.shortcuts.print_formatted_text`. |
|
|
|
This function automatically handles printing the text without interrupting the |
|
current prompt. |
|
|
|
Args: |
|
formatted_text: A list of formatted_text. |
|
style: Style to apply to `formatted_text` in :class:`dictionary` form. |
|
|
|
Example: |
|
>>> color_print(formatted_text=[("class:aa", "hello "), ("class:bb", "world")], style={"aa": "red", "bb": "blue"}) |
|
>>> color_print([("red", "yes"), ("", " "), ("blue", "no")]) |
|
""" |
|
|
|
def _print(): |
|
print_formatted_text( |
|
FormattedText(formatted_text), |
|
style=Style.from_dict(style) if style else None, |
|
) |
|
|
|
if get_app().is_running: |
|
run_in_terminal(_print) |
|
else: |
|
_print() |
|
|