Source code for maestral.cli.dialogs

This module provides interactive commandline dialogs which are based on the
:mod:`survey` Python library.
from __future__ import annotations

import os
import functools
from typing import Callable, Sequence, TypeVar
from typing_extensions import ParamSpec

[docs] P = ParamSpec("P")
[docs] T = TypeVar("T")
def _style_message(message: str) -> str: return f"{message} " def _style_hint(hint: str) -> str: return f"{hint} " if hint else "" def _style_error(message: str) -> str: import survey return survey.utils.paint(survey.colors.basic("red"), message)
[docs] def exit_on_keyboard_interrupt(func: Callable[P, T]) -> Callable[P, T]: @functools.wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: import survey try: return func(*args, **kwargs) except (KeyboardInterrupt, survey.widgets.Escape): raise SystemExit("Aborted") return wrapper
[docs] def prompt( message: str, validate: Callable[[str], bool] | None = None, ) -> str: import survey def check(value: str) -> None: if validate is not None and not validate(value): raise survey.widgets.Abort(_style_error(f"'{value}' is not allowed")) return survey.routines.input( _style_message(message), validate=check, escapable=True, mark_color=survey.colors.basic("cyan"), )
[docs] def confirm(message: str, default: bool | None = True) -> bool: import survey default_to_str = {True: "y", False: "n", None: None} return survey.routines.inquire( _style_message(message), default=default_to_str[default], escapable=True, mark_color=survey.colors.basic("cyan"), )
[docs] def select(message: str, options: Sequence[str], hint: str | None = "") -> int: import survey if hint is None: kwargs = {} else: kwargs = {"hint": _style_hint(hint)} return _style_message(message), options=options, escapable=True, mark_color=survey.colors.basic("cyan"), **kwargs, )
[docs] def select_multiple( message: str, options: Sequence[str], hint: str | None = None ) -> list[int]: import survey if hint is None: kwargs = {} else: kwargs = {"hint": _style_hint(hint)} def reply(widget: survey.widgets.Widget, indices: set[int]) -> str: chosen = [options[index] for index in indices] response = ", ".join(chosen) if len(indices) == 0 or len(response) > 10: response = f"[{len(indices)} chosen]" return survey.utils.paint(survey.colors.basic("cyan"), response) return survey.routines.basket( _style_message(message), options=options, positive_mark="[✓] ", negative_mark="[ ] ", reply=reply, escapable=True, mark_color=survey.colors.basic("cyan"), **kwargs, )
[docs] def select_path( message: str, default: str | None = None, validate: Callable[[str], bool] = lambda x: True, exists: bool = False, files_allowed: bool = True, dirs_allowed: bool = True, ) -> str: import survey styled_message = _style_message(message) def check(value: str) -> None: value = value.strip() if value == "" and default: return full_path = os.path.expanduser(value) forbidden_dir = os.path.isdir(full_path) and not dirs_allowed forbidden_file = os.path.isfile(full_path) and not files_allowed exist_condition = os.path.exists(full_path) or not exists if not exist_condition: raise survey.widgets.Abort(_style_error(f"'{value}' does not exist")) elif forbidden_dir: raise survey.widgets.Abort(_style_error(f"'{value}' is not a file")) elif forbidden_file: raise survey.widgets.Abort(_style_error(f"'{value}' is not a folder")) elif not validate(value): raise survey.widgets.Abort(_style_error(f"'{value}' is not allowed")) def reply(widget: survey.widgets.Widget, value: str) -> str: return survey.utils.paint(survey.colors.basic("cyan"), value or default) kwargs = {"hint": f"[{default}] "} if default else {} result = survey.routines.input( styled_message, reply=reply, validate=check, escapable=True, mark_color=survey.colors.basic("cyan"), **kwargs, ) result = result.strip() if result == "" and default: return default elif result == "": raise RuntimeError("No result and no default") return result