1from __future__ import annotations
2
3from typing import Generic, Sequence, TypeVar
4
5from prompt_toolkit.application import Application
6from prompt_toolkit.filters import (
7 Condition,
8 FilterOrBool,
9 is_done,
10 renderer_height_is_known,
11 to_filter,
12)
13from prompt_toolkit.formatted_text import AnyFormattedText
14from prompt_toolkit.key_binding.key_bindings import (
15 DynamicKeyBindings,
16 KeyBindings,
17 KeyBindingsBase,
18 merge_key_bindings,
19)
20from prompt_toolkit.key_binding.key_processor import KeyPressEvent
21from prompt_toolkit.layout import (
22 AnyContainer,
23 ConditionalContainer,
24 HSplit,
25 Layout,
26 Window,
27)
28from prompt_toolkit.layout.controls import FormattedTextControl
29from prompt_toolkit.layout.dimension import Dimension
30from prompt_toolkit.styles import BaseStyle, Style
31from prompt_toolkit.utils import suspend_to_background_supported
32from prompt_toolkit.widgets import Box, Frame, Label, RadioList
33
34__all__ = [
35 "ChoiceInput",
36 "choice",
37]
38
39_T = TypeVar("_T")
40E = KeyPressEvent
41
42
43def create_default_choice_input_style() -> BaseStyle:
44 return Style.from_dict(
45 {
46 "frame.border": "#884444",
47 "selected-option": "bold",
48 }
49 )
50
51
52class ChoiceInput(Generic[_T]):
53 """
54 Input selection prompt. Ask the user to choose among a set of options.
55
56 Example usage::
57
58 input_selection = ChoiceInput(
59 message="Please select a dish:",
60 options=[
61 ("pizza", "Pizza with mushrooms"),
62 ("salad", "Salad with tomatoes"),
63 ("sushi", "Sushi"),
64 ],
65 default="pizza",
66 )
67 result = input_selection.prompt()
68
69 :param message: Plain text or formatted text to be shown before the options.
70 :param options: Sequence of ``(value, label)`` tuples. The labels can be
71 formatted text.
72 :param default: Default value. If none is given, the first option is
73 considered the default.
74 :param mouse_support: Enable mouse support.
75 :param style: :class:`.Style` instance for the color scheme.
76 :param symbol: Symbol to be displayed in front of the selected choice.
77 :param bottom_toolbar: Formatted text or callable that returns formatted
78 text to be displayed at the bottom of the screen.
79 :param show_frame: `bool` or
80 :class:`~prompt_toolkit.filters.Filter`. When True, surround the input
81 with a frame.
82 :param enable_interrupt: `bool` or
83 :class:`~prompt_toolkit.filters.Filter`. When True, raise
84 the ``interrupt_exception`` (``KeyboardInterrupt`` by default) when
85 control-c has been pressed.
86 :param interrupt_exception: The exception type that will be raised when
87 there is a keyboard interrupt (control-c keypress).
88 """
89
90 def __init__(
91 self,
92 *,
93 message: AnyFormattedText,
94 options: Sequence[tuple[_T, AnyFormattedText]],
95 default: _T | None = None,
96 mouse_support: bool = False,
97 style: BaseStyle | None = None,
98 symbol: str = ">",
99 bottom_toolbar: AnyFormattedText = None,
100 show_frame: FilterOrBool = False,
101 enable_suspend: FilterOrBool = False,
102 enable_interrupt: FilterOrBool = True,
103 interrupt_exception: type[BaseException] = KeyboardInterrupt,
104 key_bindings: KeyBindingsBase | None = None,
105 ) -> None:
106 if style is None:
107 style = create_default_choice_input_style()
108
109 self.message = message
110 self.default = default
111 self.options = options
112 self.mouse_support = mouse_support
113 self.style = style
114 self.symbol = symbol
115 self.show_frame = show_frame
116 self.enable_suspend = enable_suspend
117 self.interrupt_exception = interrupt_exception
118 self.enable_interrupt = enable_interrupt
119 self.bottom_toolbar = bottom_toolbar
120 self.key_bindings = key_bindings
121
122 def _create_application(self) -> Application[_T]:
123 radio_list = RadioList(
124 values=self.options,
125 default=self.default,
126 select_on_focus=True,
127 open_character="",
128 select_character=self.symbol,
129 close_character="",
130 show_cursor=False,
131 show_numbers=True,
132 container_style="class:input-selection",
133 default_style="class:option",
134 selected_style="",
135 checked_style="class:selected-option",
136 number_style="class:number",
137 show_scrollbar=False,
138 )
139 container: AnyContainer = HSplit(
140 [
141 Box(
142 Label(text=self.message, dont_extend_height=True),
143 padding_top=0,
144 padding_left=1,
145 padding_right=1,
146 padding_bottom=0,
147 ),
148 Box(
149 radio_list,
150 padding_top=0,
151 padding_left=3,
152 padding_right=1,
153 padding_bottom=0,
154 ),
155 ]
156 )
157
158 @Condition
159 def show_frame_filter() -> bool:
160 return to_filter(self.show_frame)()
161
162 show_bottom_toolbar = (
163 Condition(lambda: self.bottom_toolbar is not None)
164 & ~is_done
165 & renderer_height_is_known
166 )
167
168 container = ConditionalContainer(
169 Frame(container),
170 alternative_content=container,
171 filter=show_frame_filter,
172 )
173
174 bottom_toolbar = ConditionalContainer(
175 Window(
176 FormattedTextControl(
177 lambda: self.bottom_toolbar, style="class:bottom-toolbar.text"
178 ),
179 style="class:bottom-toolbar",
180 dont_extend_height=True,
181 height=Dimension(min=1),
182 ),
183 filter=show_bottom_toolbar,
184 )
185
186 layout = Layout(
187 HSplit(
188 [
189 container,
190 # Add an empty window between the selection input and the
191 # bottom toolbar, if the bottom toolbar is visible, in
192 # order to allow the bottom toolbar to be displayed at the
193 # bottom of the screen.
194 ConditionalContainer(Window(), filter=show_bottom_toolbar),
195 bottom_toolbar,
196 ]
197 ),
198 focused_element=radio_list,
199 )
200
201 kb = KeyBindings()
202
203 @kb.add("enter", eager=True)
204 def _accept_input(event: E) -> None:
205 "Accept input when enter has been pressed."
206 event.app.exit(result=radio_list.current_value, style="class:accepted")
207
208 @Condition
209 def enable_interrupt() -> bool:
210 return to_filter(self.enable_interrupt)()
211
212 @kb.add("c-c", filter=enable_interrupt)
213 @kb.add("<sigint>", filter=enable_interrupt)
214 def _keyboard_interrupt(event: E) -> None:
215 "Abort when Control-C has been pressed."
216 event.app.exit(exception=self.interrupt_exception(), style="class:aborting")
217
218 suspend_supported = Condition(suspend_to_background_supported)
219
220 @Condition
221 def enable_suspend() -> bool:
222 return to_filter(self.enable_suspend)()
223
224 @kb.add("c-z", filter=suspend_supported & enable_suspend)
225 def _suspend(event: E) -> None:
226 """
227 Suspend process to background.
228 """
229 event.app.suspend_to_background()
230
231 return Application(
232 layout=layout,
233 full_screen=False,
234 mouse_support=self.mouse_support,
235 key_bindings=merge_key_bindings(
236 [kb, DynamicKeyBindings(lambda: self.key_bindings)]
237 ),
238 style=self.style,
239 )
240
241 def prompt(self) -> _T:
242 return self._create_application().run()
243
244 async def prompt_async(self) -> _T:
245 return await self._create_application().run_async()
246
247
248def choice(
249 message: AnyFormattedText,
250 *,
251 options: Sequence[tuple[_T, AnyFormattedText]],
252 default: _T | None = None,
253 mouse_support: bool = False,
254 style: BaseStyle | None = None,
255 symbol: str = ">",
256 bottom_toolbar: AnyFormattedText = None,
257 show_frame: bool = False,
258 enable_suspend: FilterOrBool = False,
259 enable_interrupt: FilterOrBool = True,
260 interrupt_exception: type[BaseException] = KeyboardInterrupt,
261 key_bindings: KeyBindingsBase | None = None,
262) -> _T:
263 """
264 Choice selection prompt. Ask the user to choose among a set of options.
265
266 Example usage::
267
268 result = choice(
269 message="Please select a dish:",
270 options=[
271 ("pizza", "Pizza with mushrooms"),
272 ("salad", "Salad with tomatoes"),
273 ("sushi", "Sushi"),
274 ],
275 default="pizza",
276 )
277
278 :param message: Plain text or formatted text to be shown before the options.
279 :param options: Sequence of ``(value, label)`` tuples. The labels can be
280 formatted text.
281 :param default: Default value. If none is given, the first option is
282 considered the default.
283 :param mouse_support: Enable mouse support.
284 :param style: :class:`.Style` instance for the color scheme.
285 :param symbol: Symbol to be displayed in front of the selected choice.
286 :param bottom_toolbar: Formatted text or callable that returns formatted
287 text to be displayed at the bottom of the screen.
288 :param show_frame: `bool` or
289 :class:`~prompt_toolkit.filters.Filter`. When True, surround the input
290 with a frame.
291 :param enable_interrupt: `bool` or
292 :class:`~prompt_toolkit.filters.Filter`. When True, raise
293 the ``interrupt_exception`` (``KeyboardInterrupt`` by default) when
294 control-c has been pressed.
295 :param interrupt_exception: The exception type that will be raised when
296 there is a keyboard interrupt (control-c keypress).
297 """
298 return ChoiceInput[_T](
299 message=message,
300 options=options,
301 default=default,
302 mouse_support=mouse_support,
303 style=style,
304 symbol=symbol,
305 bottom_toolbar=bottom_toolbar,
306 show_frame=show_frame,
307 enable_suspend=enable_suspend,
308 enable_interrupt=enable_interrupt,
309 interrupt_exception=interrupt_exception,
310 key_bindings=key_bindings,
311 ).prompt()