choice_input.py 10 KB

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