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