dialogs.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. from __future__ import annotations
  2. import functools
  3. from asyncio import get_running_loop
  4. from typing import Any, Callable, Sequence, TypeVar
  5. from prompt_toolkit.application import Application
  6. from prompt_toolkit.application.current import get_app
  7. from prompt_toolkit.buffer import Buffer
  8. from prompt_toolkit.completion import Completer
  9. from prompt_toolkit.eventloop import run_in_executor_with_context
  10. from prompt_toolkit.filters import FilterOrBool
  11. from prompt_toolkit.formatted_text import AnyFormattedText
  12. from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous
  13. from prompt_toolkit.key_binding.defaults import load_key_bindings
  14. from prompt_toolkit.key_binding.key_bindings import KeyBindings, merge_key_bindings
  15. from prompt_toolkit.layout import Layout
  16. from prompt_toolkit.layout.containers import AnyContainer, HSplit
  17. from prompt_toolkit.layout.dimension import Dimension as D
  18. from prompt_toolkit.styles import BaseStyle
  19. from prompt_toolkit.validation import Validator
  20. from prompt_toolkit.widgets import (
  21. Box,
  22. Button,
  23. CheckboxList,
  24. Dialog,
  25. Label,
  26. ProgressBar,
  27. RadioList,
  28. TextArea,
  29. ValidationToolbar,
  30. )
  31. __all__ = [
  32. "yes_no_dialog",
  33. "button_dialog",
  34. "input_dialog",
  35. "message_dialog",
  36. "radiolist_dialog",
  37. "checkboxlist_dialog",
  38. "progress_dialog",
  39. ]
  40. def yes_no_dialog(
  41. title: AnyFormattedText = "",
  42. text: AnyFormattedText = "",
  43. yes_text: str = "Yes",
  44. no_text: str = "No",
  45. style: BaseStyle | None = None,
  46. ) -> Application[bool]:
  47. """
  48. Display a Yes/No dialog.
  49. Return a boolean.
  50. """
  51. def yes_handler() -> None:
  52. get_app().exit(result=True)
  53. def no_handler() -> None:
  54. get_app().exit(result=False)
  55. dialog = Dialog(
  56. title=title,
  57. body=Label(text=text, dont_extend_height=True),
  58. buttons=[
  59. Button(text=yes_text, handler=yes_handler),
  60. Button(text=no_text, handler=no_handler),
  61. ],
  62. with_background=True,
  63. )
  64. return _create_app(dialog, style)
  65. _T = TypeVar("_T")
  66. def button_dialog(
  67. title: AnyFormattedText = "",
  68. text: AnyFormattedText = "",
  69. buttons: list[tuple[str, _T]] = [],
  70. style: BaseStyle | None = None,
  71. ) -> Application[_T]:
  72. """
  73. Display a dialog with button choices (given as a list of tuples).
  74. Return the value associated with button.
  75. """
  76. def button_handler(v: _T) -> None:
  77. get_app().exit(result=v)
  78. dialog = Dialog(
  79. title=title,
  80. body=Label(text=text, dont_extend_height=True),
  81. buttons=[
  82. Button(text=t, handler=functools.partial(button_handler, v))
  83. for t, v in buttons
  84. ],
  85. with_background=True,
  86. )
  87. return _create_app(dialog, style)
  88. def input_dialog(
  89. title: AnyFormattedText = "",
  90. text: AnyFormattedText = "",
  91. ok_text: str = "OK",
  92. cancel_text: str = "Cancel",
  93. completer: Completer | None = None,
  94. validator: Validator | None = None,
  95. password: FilterOrBool = False,
  96. style: BaseStyle | None = None,
  97. default: str = "",
  98. ) -> Application[str]:
  99. """
  100. Display a text input box.
  101. Return the given text, or None when cancelled.
  102. """
  103. def accept(buf: Buffer) -> bool:
  104. get_app().layout.focus(ok_button)
  105. return True # Keep text.
  106. def ok_handler() -> None:
  107. get_app().exit(result=textfield.text)
  108. ok_button = Button(text=ok_text, handler=ok_handler)
  109. cancel_button = Button(text=cancel_text, handler=_return_none)
  110. textfield = TextArea(
  111. text=default,
  112. multiline=False,
  113. password=password,
  114. completer=completer,
  115. validator=validator,
  116. accept_handler=accept,
  117. )
  118. dialog = Dialog(
  119. title=title,
  120. body=HSplit(
  121. [
  122. Label(text=text, dont_extend_height=True),
  123. textfield,
  124. ValidationToolbar(),
  125. ],
  126. padding=D(preferred=1, max=1),
  127. ),
  128. buttons=[ok_button, cancel_button],
  129. with_background=True,
  130. )
  131. return _create_app(dialog, style)
  132. def message_dialog(
  133. title: AnyFormattedText = "",
  134. text: AnyFormattedText = "",
  135. ok_text: str = "Ok",
  136. style: BaseStyle | None = None,
  137. ) -> Application[None]:
  138. """
  139. Display a simple message box and wait until the user presses enter.
  140. """
  141. dialog = Dialog(
  142. title=title,
  143. body=Label(text=text, dont_extend_height=True),
  144. buttons=[Button(text=ok_text, handler=_return_none)],
  145. with_background=True,
  146. )
  147. return _create_app(dialog, style)
  148. def radiolist_dialog(
  149. title: AnyFormattedText = "",
  150. text: AnyFormattedText = "",
  151. ok_text: str = "Ok",
  152. cancel_text: str = "Cancel",
  153. values: Sequence[tuple[_T, AnyFormattedText]] | None = None,
  154. default: _T | None = None,
  155. style: BaseStyle | None = None,
  156. ) -> Application[_T]:
  157. """
  158. Display a simple list of element the user can choose amongst.
  159. Only one element can be selected at a time using Arrow keys and Enter.
  160. The focus can be moved between the list and the Ok/Cancel button with tab.
  161. """
  162. if values is None:
  163. values = []
  164. def ok_handler() -> None:
  165. get_app().exit(result=radio_list.current_value)
  166. radio_list = RadioList(values=values, default=default)
  167. dialog = Dialog(
  168. title=title,
  169. body=HSplit(
  170. [Label(text=text, dont_extend_height=True), radio_list],
  171. padding=1,
  172. ),
  173. buttons=[
  174. Button(text=ok_text, handler=ok_handler),
  175. Button(text=cancel_text, handler=_return_none),
  176. ],
  177. with_background=True,
  178. )
  179. return _create_app(dialog, style)
  180. def checkboxlist_dialog(
  181. title: AnyFormattedText = "",
  182. text: AnyFormattedText = "",
  183. ok_text: str = "Ok",
  184. cancel_text: str = "Cancel",
  185. values: Sequence[tuple[_T, AnyFormattedText]] | None = None,
  186. default_values: Sequence[_T] | None = None,
  187. style: BaseStyle | None = None,
  188. ) -> Application[list[_T]]:
  189. """
  190. Display a simple list of element the user can choose multiple values amongst.
  191. Several elements can be selected at a time using Arrow keys and Enter.
  192. The focus can be moved between the list and the Ok/Cancel button with tab.
  193. """
  194. if values is None:
  195. values = []
  196. def ok_handler() -> None:
  197. get_app().exit(result=cb_list.current_values)
  198. cb_list = CheckboxList(values=values, default_values=default_values)
  199. dialog = Dialog(
  200. title=title,
  201. body=HSplit(
  202. [Label(text=text, dont_extend_height=True), cb_list],
  203. padding=1,
  204. ),
  205. buttons=[
  206. Button(text=ok_text, handler=ok_handler),
  207. Button(text=cancel_text, handler=_return_none),
  208. ],
  209. with_background=True,
  210. )
  211. return _create_app(dialog, style)
  212. def progress_dialog(
  213. title: AnyFormattedText = "",
  214. text: AnyFormattedText = "",
  215. run_callback: Callable[[Callable[[int], None], Callable[[str], None]], None] = (
  216. lambda *a: None
  217. ),
  218. style: BaseStyle | None = None,
  219. ) -> Application[None]:
  220. """
  221. :param run_callback: A function that receives as input a `set_percentage`
  222. function and it does the work.
  223. """
  224. loop = get_running_loop()
  225. progressbar = ProgressBar()
  226. text_area = TextArea(
  227. focusable=False,
  228. # Prefer this text area as big as possible, to avoid having a window
  229. # that keeps resizing when we add text to it.
  230. height=D(preferred=10**10),
  231. )
  232. dialog = Dialog(
  233. body=HSplit(
  234. [
  235. Box(Label(text=text)),
  236. Box(text_area, padding=D.exact(1)),
  237. progressbar,
  238. ]
  239. ),
  240. title=title,
  241. with_background=True,
  242. )
  243. app = _create_app(dialog, style)
  244. def set_percentage(value: int) -> None:
  245. progressbar.percentage = int(value)
  246. app.invalidate()
  247. def log_text(text: str) -> None:
  248. loop.call_soon_threadsafe(text_area.buffer.insert_text, text)
  249. app.invalidate()
  250. # Run the callback in the executor. When done, set a return value for the
  251. # UI, so that it quits.
  252. def start() -> None:
  253. try:
  254. run_callback(set_percentage, log_text)
  255. finally:
  256. app.exit()
  257. def pre_run() -> None:
  258. run_in_executor_with_context(start)
  259. app.pre_run_callables.append(pre_run)
  260. return app
  261. def _create_app(dialog: AnyContainer, style: BaseStyle | None) -> Application[Any]:
  262. # Key bindings.
  263. bindings = KeyBindings()
  264. bindings.add("tab")(focus_next)
  265. bindings.add("s-tab")(focus_previous)
  266. return Application(
  267. layout=Layout(dialog),
  268. key_bindings=merge_key_bindings([load_key_bindings(), bindings]),
  269. mouse_support=True,
  270. style=style,
  271. full_screen=True,
  272. )
  273. def _return_none() -> None:
  274. "Button handler that returns None."
  275. get_app().exit()