Mini Shell
from __future__ import annotations
import functools
from asyncio import get_running_loop
from typing import Any, Callable, Sequence, TypeVar
from prompt_toolkit.application import Application
from prompt_toolkit.application.current import get_app
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.completion import Completer
from prompt_toolkit.eventloop import run_in_executor_with_context
from prompt_toolkit.filters import FilterOrBool
from prompt_toolkit.formatted_text import AnyFormattedText
from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous
from prompt_toolkit.key_binding.defaults import load_key_bindings
from prompt_toolkit.key_binding.key_bindings import KeyBindings, merge_key_bindings
from prompt_toolkit.layout import Layout
from prompt_toolkit.layout.containers import AnyContainer, HSplit
from prompt_toolkit.layout.dimension import Dimension as D
from prompt_toolkit.styles import BaseStyle
from prompt_toolkit.validation import Validator
from prompt_toolkit.widgets import (
Box,
Button,
CheckboxList,
Dialog,
Label,
ProgressBar,
RadioList,
TextArea,
ValidationToolbar,
)
__all__ = [
"yes_no_dialog",
"button_dialog",
"input_dialog",
"message_dialog",
"radiolist_dialog",
"checkboxlist_dialog",
"progress_dialog",
]
def yes_no_dialog(
title: AnyFormattedText = "",
text: AnyFormattedText = "",
yes_text: str = "Yes",
no_text: str = "No",
style: BaseStyle | None = None,
) -> Application[bool]:
"""
Display a Yes/No dialog.
Return a boolean.
"""
def yes_handler() -> None:
get_app().exit(result=True)
def no_handler() -> None:
get_app().exit(result=False)
dialog = Dialog(
title=title,
body=Label(text=text, dont_extend_height=True),
buttons=[
Button(text=yes_text, handler=yes_handler),
Button(text=no_text, handler=no_handler),
],
with_background=True,
)
return _create_app(dialog, style)
_T = TypeVar("_T")
def button_dialog(
title: AnyFormattedText = "",
text: AnyFormattedText = "",
buttons: list[tuple[str, _T]] = [],
style: BaseStyle | None = None,
) -> Application[_T]:
"""
Display a dialog with button choices (given as a list of tuples).
Return the value associated with button.
"""
def button_handler(v: _T) -> None:
get_app().exit(result=v)
dialog = Dialog(
title=title,
body=Label(text=text, dont_extend_height=True),
buttons=[
Button(text=t, handler=functools.partial(button_handler, v))
for t, v in buttons
],
with_background=True,
)
return _create_app(dialog, style)
def input_dialog(
title: AnyFormattedText = "",
text: AnyFormattedText = "",
ok_text: str = "OK",
cancel_text: str = "Cancel",
completer: Completer | None = None,
validator: Validator | None = None,
password: FilterOrBool = False,
style: BaseStyle | None = None,
default: str = "",
) -> Application[str]:
"""
Display a text input box.
Return the given text, or None when cancelled.
"""
def accept(buf: Buffer) -> bool:
get_app().layout.focus(ok_button)
return True # Keep text.
def ok_handler() -> None:
get_app().exit(result=textfield.text)
ok_button = Button(text=ok_text, handler=ok_handler)
cancel_button = Button(text=cancel_text, handler=_return_none)
textfield = TextArea(
text=default,
multiline=False,
password=password,
completer=completer,
validator=validator,
accept_handler=accept,
)
dialog = Dialog(
title=title,
body=HSplit(
[
Label(text=text, dont_extend_height=True),
textfield,
ValidationToolbar(),
],
padding=D(preferred=1, max=1),
),
buttons=[ok_button, cancel_button],
with_background=True,
)
return _create_app(dialog, style)
def message_dialog(
title: AnyFormattedText = "",
text: AnyFormattedText = "",
ok_text: str = "Ok",
style: BaseStyle | None = None,
) -> Application[None]:
"""
Display a simple message box and wait until the user presses enter.
"""
dialog = Dialog(
title=title,
body=Label(text=text, dont_extend_height=True),
buttons=[Button(text=ok_text, handler=_return_none)],
with_background=True,
)
return _create_app(dialog, style)
def radiolist_dialog(
title: AnyFormattedText = "",
text: AnyFormattedText = "",
ok_text: str = "Ok",
cancel_text: str = "Cancel",
values: Sequence[tuple[_T, AnyFormattedText]] | None = None,
default: _T | None = None,
style: BaseStyle | None = None,
) -> Application[_T]:
"""
Display a simple list of element the user can choose amongst.
Only one element can be selected at a time using Arrow keys and Enter.
The focus can be moved between the list and the Ok/Cancel button with tab.
"""
if values is None:
values = []
def ok_handler() -> None:
get_app().exit(result=radio_list.current_value)
radio_list = RadioList(values=values, default=default)
dialog = Dialog(
title=title,
body=HSplit(
[Label(text=text, dont_extend_height=True), radio_list],
padding=1,
),
buttons=[
Button(text=ok_text, handler=ok_handler),
Button(text=cancel_text, handler=_return_none),
],
with_background=True,
)
return _create_app(dialog, style)
def checkboxlist_dialog(
title: AnyFormattedText = "",
text: AnyFormattedText = "",
ok_text: str = "Ok",
cancel_text: str = "Cancel",
values: Sequence[tuple[_T, AnyFormattedText]] | None = None,
default_values: Sequence[_T] | None = None,
style: BaseStyle | None = None,
) -> Application[list[_T]]:
"""
Display a simple list of element the user can choose multiple values amongst.
Several elements can be selected at a time using Arrow keys and Enter.
The focus can be moved between the list and the Ok/Cancel button with tab.
"""
if values is None:
values = []
def ok_handler() -> None:
get_app().exit(result=cb_list.current_values)
cb_list = CheckboxList(values=values, default_values=default_values)
dialog = Dialog(
title=title,
body=HSplit(
[Label(text=text, dont_extend_height=True), cb_list],
padding=1,
),
buttons=[
Button(text=ok_text, handler=ok_handler),
Button(text=cancel_text, handler=_return_none),
],
with_background=True,
)
return _create_app(dialog, style)
def progress_dialog(
title: AnyFormattedText = "",
text: AnyFormattedText = "",
run_callback: Callable[[Callable[[int], None], Callable[[str], None]], None] = (
lambda *a: None
),
style: BaseStyle | None = None,
) -> Application[None]:
"""
:param run_callback: A function that receives as input a `set_percentage`
function and it does the work.
"""
loop = get_running_loop()
progressbar = ProgressBar()
text_area = TextArea(
focusable=False,
# Prefer this text area as big as possible, to avoid having a window
# that keeps resizing when we add text to it.
height=D(preferred=10**10),
)
dialog = Dialog(
body=HSplit(
[
Box(Label(text=text)),
Box(text_area, padding=D.exact(1)),
progressbar,
]
),
title=title,
with_background=True,
)
app = _create_app(dialog, style)
def set_percentage(value: int) -> None:
progressbar.percentage = int(value)
app.invalidate()
def log_text(text: str) -> None:
loop.call_soon_threadsafe(text_area.buffer.insert_text, text)
app.invalidate()
# Run the callback in the executor. When done, set a return value for the
# UI, so that it quits.
def start() -> None:
try:
run_callback(set_percentage, log_text)
finally:
app.exit()
def pre_run() -> None:
run_in_executor_with_context(start)
app.pre_run_callables.append(pre_run)
return app
def _create_app(dialog: AnyContainer, style: BaseStyle | None) -> Application[Any]:
# Key bindings.
bindings = KeyBindings()
bindings.add("tab")(focus_next)
bindings.add("s-tab")(focus_previous)
return Application(
layout=Layout(dialog),
key_bindings=merge_key_bindings([load_key_bindings(), bindings]),
mouse_support=True,
style=style,
full_screen=True,
)
def _return_none() -> None:
"Button handler that returns None."
get_app().exit()
Zerion Mini Shell 1.0