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()