1""" 
    2Collection of reusable components for building full screen applications. 
    3 
    4All of these widgets implement the ``__pt_container__`` method, which makes 
    5them usable in any situation where we are expecting a `prompt_toolkit` 
    6container object. 
    7 
    8.. warning:: 
    9 
    10    At this point, the API for these widgets is considered unstable, and can 
    11    potentially change between minor releases (we try not too, but no 
    12    guarantees are made yet). The public API in 
    13    `prompt_toolkit.shortcuts.dialogs` on the other hand is considered stable. 
    14""" 
    15 
    16from __future__ import annotations 
    17 
    18from functools import partial 
    19from typing import Callable, Generic, Sequence, TypeVar 
    20 
    21from prompt_toolkit.application.current import get_app 
    22from prompt_toolkit.auto_suggest import AutoSuggest, DynamicAutoSuggest 
    23from prompt_toolkit.buffer import Buffer, BufferAcceptHandler 
    24from prompt_toolkit.completion import Completer, DynamicCompleter 
    25from prompt_toolkit.document import Document 
    26from prompt_toolkit.filters import ( 
    27    Condition, 
    28    FilterOrBool, 
    29    has_focus, 
    30    is_done, 
    31    is_true, 
    32    to_filter, 
    33) 
    34from prompt_toolkit.formatted_text import ( 
    35    AnyFormattedText, 
    36    StyleAndTextTuples, 
    37    Template, 
    38    to_formatted_text, 
    39) 
    40from prompt_toolkit.formatted_text.utils import fragment_list_to_text 
    41from prompt_toolkit.history import History 
    42from prompt_toolkit.key_binding.key_bindings import KeyBindings 
    43from prompt_toolkit.key_binding.key_processor import KeyPressEvent 
    44from prompt_toolkit.keys import Keys 
    45from prompt_toolkit.layout.containers import ( 
    46    AnyContainer, 
    47    ConditionalContainer, 
    48    Container, 
    49    DynamicContainer, 
    50    Float, 
    51    FloatContainer, 
    52    HSplit, 
    53    VSplit, 
    54    Window, 
    55    WindowAlign, 
    56) 
    57from prompt_toolkit.layout.controls import ( 
    58    BufferControl, 
    59    FormattedTextControl, 
    60    GetLinePrefixCallable, 
    61) 
    62from prompt_toolkit.layout.dimension import AnyDimension 
    63from prompt_toolkit.layout.dimension import Dimension as D 
    64from prompt_toolkit.layout.margins import ( 
    65    ConditionalMargin, 
    66    NumberedMargin, 
    67    ScrollbarMargin, 
    68) 
    69from prompt_toolkit.layout.processors import ( 
    70    AppendAutoSuggestion, 
    71    BeforeInput, 
    72    ConditionalProcessor, 
    73    PasswordProcessor, 
    74    Processor, 
    75) 
    76from prompt_toolkit.lexers import DynamicLexer, Lexer 
    77from prompt_toolkit.mouse_events import MouseEvent, MouseEventType 
    78from prompt_toolkit.utils import get_cwidth 
    79from prompt_toolkit.validation import DynamicValidator, Validator 
    80 
    81from .toolbars import SearchToolbar 
    82 
    83__all__ = [ 
    84    "TextArea", 
    85    "Label", 
    86    "Button", 
    87    "Frame", 
    88    "Shadow", 
    89    "Box", 
    90    "VerticalLine", 
    91    "HorizontalLine", 
    92    "RadioList", 
    93    "CheckboxList", 
    94    "Checkbox",  # backward compatibility 
    95    "ProgressBar", 
    96] 
    97 
    98E = KeyPressEvent 
    99 
    100 
    101class Border: 
    102    "Box drawing characters. (Thin)" 
    103 
    104    HORIZONTAL = "\u2500" 
    105    VERTICAL = "\u2502" 
    106    TOP_LEFT = "\u250c" 
    107    TOP_RIGHT = "\u2510" 
    108    BOTTOM_LEFT = "\u2514" 
    109    BOTTOM_RIGHT = "\u2518" 
    110 
    111 
    112class TextArea: 
    113    """ 
    114    A simple input field. 
    115 
    116    This is a higher level abstraction on top of several other classes with 
    117    sane defaults. 
    118 
    119    This widget does have the most common options, but it does not intend to 
    120    cover every single use case. For more configurations options, you can 
    121    always build a text area manually, using a 
    122    :class:`~prompt_toolkit.buffer.Buffer`, 
    123    :class:`~prompt_toolkit.layout.BufferControl` and 
    124    :class:`~prompt_toolkit.layout.Window`. 
    125 
    126    Buffer attributes: 
    127 
    128    :param text: The initial text. 
    129    :param multiline: If True, allow multiline input. 
    130    :param completer: :class:`~prompt_toolkit.completion.Completer` instance 
    131        for auto completion. 
    132    :param complete_while_typing: Boolean. 
    133    :param accept_handler: Called when `Enter` is pressed (This should be a 
    134        callable that takes a buffer as input). 
    135    :param history: :class:`~prompt_toolkit.history.History` instance. 
    136    :param auto_suggest: :class:`~prompt_toolkit.auto_suggest.AutoSuggest` 
    137        instance for input suggestions. 
    138 
    139    BufferControl attributes: 
    140 
    141    :param password: When `True`, display using asterisks. 
    142    :param focusable: When `True`, allow this widget to receive the focus. 
    143    :param focus_on_click: When `True`, focus after mouse click. 
    144    :param input_processors: `None` or a list of 
    145        :class:`~prompt_toolkit.layout.Processor` objects. 
    146    :param validator: `None` or a :class:`~prompt_toolkit.validation.Validator` 
    147        object. 
    148 
    149    Window attributes: 
    150 
    151    :param lexer: :class:`~prompt_toolkit.lexers.Lexer` instance for syntax 
    152        highlighting. 
    153    :param wrap_lines: When `True`, don't scroll horizontally, but wrap lines. 
    154    :param width: Window width. (:class:`~prompt_toolkit.layout.Dimension` object.) 
    155    :param height: Window height. (:class:`~prompt_toolkit.layout.Dimension` object.) 
    156    :param scrollbar: When `True`, display a scroll bar. 
    157    :param style: A style string. 
    158    :param dont_extend_width: When `True`, don't take up more width then the 
    159                              preferred width reported by the control. 
    160    :param dont_extend_height: When `True`, don't take up more width then the 
    161                               preferred height reported by the control. 
    162    :param get_line_prefix: None or a callable that returns formatted text to 
    163        be inserted before a line. It takes a line number (int) and a 
    164        wrap_count and returns formatted text. This can be used for 
    165        implementation of line continuations, things like Vim "breakindent" and 
    166        so on. 
    167 
    168    Other attributes: 
    169 
    170    :param search_field: An optional `SearchToolbar` object. 
    171    """ 
    172 
    173    def __init__( 
    174        self, 
    175        text: str = "", 
    176        multiline: FilterOrBool = True, 
    177        password: FilterOrBool = False, 
    178        lexer: Lexer | None = None, 
    179        auto_suggest: AutoSuggest | None = None, 
    180        completer: Completer | None = None, 
    181        complete_while_typing: FilterOrBool = True, 
    182        validator: Validator | None = None, 
    183        accept_handler: BufferAcceptHandler | None = None, 
    184        history: History | None = None, 
    185        focusable: FilterOrBool = True, 
    186        focus_on_click: FilterOrBool = False, 
    187        wrap_lines: FilterOrBool = True, 
    188        read_only: FilterOrBool = False, 
    189        width: AnyDimension = None, 
    190        height: AnyDimension = None, 
    191        dont_extend_height: FilterOrBool = False, 
    192        dont_extend_width: FilterOrBool = False, 
    193        line_numbers: bool = False, 
    194        get_line_prefix: GetLinePrefixCallable | None = None, 
    195        scrollbar: bool = False, 
    196        style: str = "", 
    197        search_field: SearchToolbar | None = None, 
    198        preview_search: FilterOrBool = True, 
    199        prompt: AnyFormattedText = "", 
    200        input_processors: list[Processor] | None = None, 
    201        name: str = "", 
    202    ) -> None: 
    203        if search_field is None: 
    204            search_control = None 
    205        elif isinstance(search_field, SearchToolbar): 
    206            search_control = search_field.control 
    207 
    208        if input_processors is None: 
    209            input_processors = [] 
    210 
    211        # Writeable attributes. 
    212        self.completer = completer 
    213        self.complete_while_typing = complete_while_typing 
    214        self.lexer = lexer 
    215        self.auto_suggest = auto_suggest 
    216        self.read_only = read_only 
    217        self.wrap_lines = wrap_lines 
    218        self.validator = validator 
    219 
    220        self.buffer = Buffer( 
    221            document=Document(text, 0), 
    222            multiline=multiline, 
    223            read_only=Condition(lambda: is_true(self.read_only)), 
    224            completer=DynamicCompleter(lambda: self.completer), 
    225            complete_while_typing=Condition( 
    226                lambda: is_true(self.complete_while_typing) 
    227            ), 
    228            validator=DynamicValidator(lambda: self.validator), 
    229            auto_suggest=DynamicAutoSuggest(lambda: self.auto_suggest), 
    230            accept_handler=accept_handler, 
    231            history=history, 
    232            name=name, 
    233        ) 
    234 
    235        self.control = BufferControl( 
    236            buffer=self.buffer, 
    237            lexer=DynamicLexer(lambda: self.lexer), 
    238            input_processors=[ 
    239                ConditionalProcessor( 
    240                    AppendAutoSuggestion(), has_focus(self.buffer) & ~is_done 
    241                ), 
    242                ConditionalProcessor( 
    243                    processor=PasswordProcessor(), filter=to_filter(password) 
    244                ), 
    245                BeforeInput(prompt, style="class:text-area.prompt"), 
    246            ] 
    247            + input_processors, 
    248            search_buffer_control=search_control, 
    249            preview_search=preview_search, 
    250            focusable=focusable, 
    251            focus_on_click=focus_on_click, 
    252        ) 
    253 
    254        if multiline: 
    255            if scrollbar: 
    256                right_margins = [ScrollbarMargin(display_arrows=True)] 
    257            else: 
    258                right_margins = [] 
    259            if line_numbers: 
    260                left_margins = [NumberedMargin()] 
    261            else: 
    262                left_margins = [] 
    263        else: 
    264            height = D.exact(1) 
    265            left_margins = [] 
    266            right_margins = [] 
    267 
    268        style = "class:text-area " + style 
    269 
    270        # If no height was given, guarantee height of at least 1. 
    271        if height is None: 
    272            height = D(min=1) 
    273 
    274        self.window = Window( 
    275            height=height, 
    276            width=width, 
    277            dont_extend_height=dont_extend_height, 
    278            dont_extend_width=dont_extend_width, 
    279            content=self.control, 
    280            style=style, 
    281            wrap_lines=Condition(lambda: is_true(self.wrap_lines)), 
    282            left_margins=left_margins, 
    283            right_margins=right_margins, 
    284            get_line_prefix=get_line_prefix, 
    285        ) 
    286 
    287    @property 
    288    def text(self) -> str: 
    289        """ 
    290        The `Buffer` text. 
    291        """ 
    292        return self.buffer.text 
    293 
    294    @text.setter 
    295    def text(self, value: str) -> None: 
    296        self.document = Document(value, 0) 
    297 
    298    @property 
    299    def document(self) -> Document: 
    300        """ 
    301        The `Buffer` document (text + cursor position). 
    302        """ 
    303        return self.buffer.document 
    304 
    305    @document.setter 
    306    def document(self, value: Document) -> None: 
    307        self.buffer.set_document(value, bypass_readonly=True) 
    308 
    309    @property 
    310    def accept_handler(self) -> BufferAcceptHandler | None: 
    311        """ 
    312        The accept handler. Called when the user accepts the input. 
    313        """ 
    314        return self.buffer.accept_handler 
    315 
    316    @accept_handler.setter 
    317    def accept_handler(self, value: BufferAcceptHandler) -> None: 
    318        self.buffer.accept_handler = value 
    319 
    320    def __pt_container__(self) -> Container: 
    321        return self.window 
    322 
    323 
    324class Label: 
    325    """ 
    326    Widget that displays the given text. It is not editable or focusable. 
    327 
    328    :param text: Text to display. Can be multiline. All value types accepted by 
    329        :class:`prompt_toolkit.layout.FormattedTextControl` are allowed, 
    330        including a callable. 
    331    :param style: A style string. 
    332    :param width: When given, use this width, rather than calculating it from 
    333        the text size. 
    334    :param dont_extend_width: When `True`, don't take up more width than 
    335                              preferred, i.e. the length of the longest line of 
    336                              the text, or value of `width` parameter, if 
    337                              given. `True` by default 
    338    :param dont_extend_height: When `True`, don't take up more width than the 
    339                               preferred height, i.e. the number of lines of 
    340                               the text. `False` by default. 
    341    """ 
    342 
    343    def __init__( 
    344        self, 
    345        text: AnyFormattedText, 
    346        style: str = "", 
    347        width: AnyDimension = None, 
    348        dont_extend_height: bool = True, 
    349        dont_extend_width: bool = False, 
    350        align: WindowAlign | Callable[[], WindowAlign] = WindowAlign.LEFT, 
    351        # There is no cursor navigation in a label, so it makes sense to always 
    352        # wrap lines by default. 
    353        wrap_lines: FilterOrBool = True, 
    354    ) -> None: 
    355        self.text = text 
    356 
    357        def get_width() -> AnyDimension: 
    358            if width is None: 
    359                text_fragments = to_formatted_text(self.text) 
    360                text = fragment_list_to_text(text_fragments) 
    361                if text: 
    362                    longest_line = max(get_cwidth(line) for line in text.splitlines()) 
    363                else: 
    364                    return D(preferred=0) 
    365                return D(preferred=longest_line) 
    366            else: 
    367                return width 
    368 
    369        self.formatted_text_control = FormattedTextControl(text=lambda: self.text) 
    370 
    371        self.window = Window( 
    372            content=self.formatted_text_control, 
    373            width=get_width, 
    374            height=D(min=1), 
    375            style="class:label " + style, 
    376            dont_extend_height=dont_extend_height, 
    377            dont_extend_width=dont_extend_width, 
    378            align=align, 
    379            wrap_lines=wrap_lines, 
    380        ) 
    381 
    382    def __pt_container__(self) -> Container: 
    383        return self.window 
    384 
    385 
    386class Button: 
    387    """ 
    388    Clickable button. 
    389 
    390    :param text: The caption for the button. 
    391    :param handler: `None` or callable. Called when the button is clicked. No 
    392        parameters are passed to this callable. Use for instance Python's 
    393        `functools.partial` to pass parameters to this callable if needed. 
    394    :param width: Width of the button. 
    395    """ 
    396 
    397    def __init__( 
    398        self, 
    399        text: str, 
    400        handler: Callable[[], None] | None = None, 
    401        width: int = 12, 
    402        left_symbol: str = "<", 
    403        right_symbol: str = ">", 
    404    ) -> None: 
    405        self.text = text 
    406        self.left_symbol = left_symbol 
    407        self.right_symbol = right_symbol 
    408        self.handler = handler 
    409        self.width = width 
    410        self.control = FormattedTextControl( 
    411            self._get_text_fragments, 
    412            key_bindings=self._get_key_bindings(), 
    413            focusable=True, 
    414        ) 
    415 
    416        def get_style() -> str: 
    417            if get_app().layout.has_focus(self): 
    418                return "class:button.focused" 
    419            else: 
    420                return "class:button" 
    421 
    422        # Note: `dont_extend_width` is False, because we want to allow buttons 
    423        #       to take more space if the parent container provides more space. 
    424        #       Otherwise, we will also truncate the text. 
    425        #       Probably we need a better way here to adjust to width of the 
    426        #       button to the text. 
    427 
    428        self.window = Window( 
    429            self.control, 
    430            align=WindowAlign.CENTER, 
    431            height=1, 
    432            width=width, 
    433            style=get_style, 
    434            dont_extend_width=False, 
    435            dont_extend_height=True, 
    436        ) 
    437 
    438    def _get_text_fragments(self) -> StyleAndTextTuples: 
    439        width = ( 
    440            self.width 
    441            - (get_cwidth(self.left_symbol) + get_cwidth(self.right_symbol)) 
    442            + (len(self.text) - get_cwidth(self.text)) 
    443        ) 
    444        text = (f"{{:^{max(0, width)}}}").format(self.text) 
    445 
    446        def handler(mouse_event: MouseEvent) -> None: 
    447            if ( 
    448                self.handler is not None 
    449                and mouse_event.event_type == MouseEventType.MOUSE_UP 
    450            ): 
    451                self.handler() 
    452 
    453        return [ 
    454            ("class:button.arrow", self.left_symbol, handler), 
    455            ("[SetCursorPosition]", ""), 
    456            ("class:button.text", text, handler), 
    457            ("class:button.arrow", self.right_symbol, handler), 
    458        ] 
    459 
    460    def _get_key_bindings(self) -> KeyBindings: 
    461        "Key bindings for the Button." 
    462        kb = KeyBindings() 
    463 
    464        @kb.add(" ") 
    465        @kb.add("enter") 
    466        def _(event: E) -> None: 
    467            if self.handler is not None: 
    468                self.handler() 
    469 
    470        return kb 
    471 
    472    def __pt_container__(self) -> Container: 
    473        return self.window 
    474 
    475 
    476class Frame: 
    477    """ 
    478    Draw a border around any container, optionally with a title text. 
    479 
    480    Changing the title and body of the frame is possible at runtime by 
    481    assigning to the `body` and `title` attributes of this class. 
    482 
    483    :param body: Another container object. 
    484    :param title: Text to be displayed in the top of the frame (can be formatted text). 
    485    :param style: Style string to be applied to this widget. 
    486    """ 
    487 
    488    def __init__( 
    489        self, 
    490        body: AnyContainer, 
    491        title: AnyFormattedText = "", 
    492        style: str = "", 
    493        width: AnyDimension = None, 
    494        height: AnyDimension = None, 
    495        key_bindings: KeyBindings | None = None, 
    496        modal: bool = False, 
    497    ) -> None: 
    498        self.title = title 
    499        self.body = body 
    500 
    501        fill = partial(Window, style="class:frame.border") 
    502        style = "class:frame " + style 
    503 
    504        top_row_with_title = VSplit( 
    505            [ 
    506                fill(width=1, height=1, char=Border.TOP_LEFT), 
    507                fill(char=Border.HORIZONTAL), 
    508                fill(width=1, height=1, char="|"), 
    509                # Notice: we use `Template` here, because `self.title` can be an 
    510                # `HTML` object for instance. 
    511                Label( 
    512                    lambda: Template(" {} ").format(self.title), 
    513                    style="class:frame.label", 
    514                    dont_extend_width=True, 
    515                ), 
    516                fill(width=1, height=1, char="|"), 
    517                fill(char=Border.HORIZONTAL), 
    518                fill(width=1, height=1, char=Border.TOP_RIGHT), 
    519            ], 
    520            height=1, 
    521        ) 
    522 
    523        top_row_without_title = VSplit( 
    524            [ 
    525                fill(width=1, height=1, char=Border.TOP_LEFT), 
    526                fill(char=Border.HORIZONTAL), 
    527                fill(width=1, height=1, char=Border.TOP_RIGHT), 
    528            ], 
    529            height=1, 
    530        ) 
    531 
    532        @Condition 
    533        def has_title() -> bool: 
    534            return bool(self.title) 
    535 
    536        self.container = HSplit( 
    537            [ 
    538                ConditionalContainer( 
    539                    content=top_row_with_title, 
    540                    filter=has_title, 
    541                    alternative_content=top_row_without_title, 
    542                ), 
    543                VSplit( 
    544                    [ 
    545                        fill(width=1, char=Border.VERTICAL), 
    546                        DynamicContainer(lambda: self.body), 
    547                        fill(width=1, char=Border.VERTICAL), 
    548                        # Padding is required to make sure that if the content is 
    549                        # too small, the right frame border is still aligned. 
    550                    ], 
    551                    padding=0, 
    552                ), 
    553                VSplit( 
    554                    [ 
    555                        fill(width=1, height=1, char=Border.BOTTOM_LEFT), 
    556                        fill(char=Border.HORIZONTAL), 
    557                        fill(width=1, height=1, char=Border.BOTTOM_RIGHT), 
    558                    ], 
    559                    # specifying height here will increase the rendering speed. 
    560                    height=1, 
    561                ), 
    562            ], 
    563            width=width, 
    564            height=height, 
    565            style=style, 
    566            key_bindings=key_bindings, 
    567            modal=modal, 
    568        ) 
    569 
    570    def __pt_container__(self) -> Container: 
    571        return self.container 
    572 
    573 
    574class Shadow: 
    575    """ 
    576    Draw a shadow underneath/behind this container. 
    577    (This applies `class:shadow` the the cells under the shadow. The Style 
    578    should define the colors for the shadow.) 
    579 
    580    :param body: Another container object. 
    581    """ 
    582 
    583    def __init__(self, body: AnyContainer) -> None: 
    584        self.container = FloatContainer( 
    585            content=body, 
    586            floats=[ 
    587                Float( 
    588                    bottom=-1, 
    589                    height=1, 
    590                    left=1, 
    591                    right=-1, 
    592                    transparent=True, 
    593                    content=Window(style="class:shadow"), 
    594                ), 
    595                Float( 
    596                    bottom=-1, 
    597                    top=1, 
    598                    width=1, 
    599                    right=-1, 
    600                    transparent=True, 
    601                    content=Window(style="class:shadow"), 
    602                ), 
    603            ], 
    604        ) 
    605 
    606    def __pt_container__(self) -> Container: 
    607        return self.container 
    608 
    609 
    610class Box: 
    611    """ 
    612    Add padding around a container. 
    613 
    614    This also makes sure that the parent can provide more space than required by 
    615    the child. This is very useful when wrapping a small element with a fixed 
    616    size into a ``VSplit`` or ``HSplit`` object. The ``HSplit`` and ``VSplit`` 
    617    try to make sure to adapt respectively the width and height, possibly 
    618    shrinking other elements. Wrapping something in a ``Box`` makes it flexible. 
    619 
    620    :param body: Another container object. 
    621    :param padding: The margin to be used around the body. This can be 
    622        overridden by `padding_left`, padding_right`, `padding_top` and 
    623        `padding_bottom`. 
    624    :param style: A style string. 
    625    :param char: Character to be used for filling the space around the body. 
    626        (This is supposed to be a character with a terminal width of 1.) 
    627    """ 
    628 
    629    def __init__( 
    630        self, 
    631        body: AnyContainer, 
    632        padding: AnyDimension = None, 
    633        padding_left: AnyDimension = None, 
    634        padding_right: AnyDimension = None, 
    635        padding_top: AnyDimension = None, 
    636        padding_bottom: AnyDimension = None, 
    637        width: AnyDimension = None, 
    638        height: AnyDimension = None, 
    639        style: str = "", 
    640        char: None | str | Callable[[], str] = None, 
    641        modal: bool = False, 
    642        key_bindings: KeyBindings | None = None, 
    643    ) -> None: 
    644        self.padding = padding 
    645        self.padding_left = padding_left 
    646        self.padding_right = padding_right 
    647        self.padding_top = padding_top 
    648        self.padding_bottom = padding_bottom 
    649        self.body = body 
    650 
    651        def left() -> AnyDimension: 
    652            if self.padding_left is None: 
    653                return self.padding 
    654            return self.padding_left 
    655 
    656        def right() -> AnyDimension: 
    657            if self.padding_right is None: 
    658                return self.padding 
    659            return self.padding_right 
    660 
    661        def top() -> AnyDimension: 
    662            if self.padding_top is None: 
    663                return self.padding 
    664            return self.padding_top 
    665 
    666        def bottom() -> AnyDimension: 
    667            if self.padding_bottom is None: 
    668                return self.padding 
    669            return self.padding_bottom 
    670 
    671        self.container = HSplit( 
    672            [ 
    673                Window(height=top, char=char), 
    674                VSplit( 
    675                    [ 
    676                        Window(width=left, char=char), 
    677                        body, 
    678                        Window(width=right, char=char), 
    679                    ] 
    680                ), 
    681                Window(height=bottom, char=char), 
    682            ], 
    683            width=width, 
    684            height=height, 
    685            style=style, 
    686            modal=modal, 
    687            key_bindings=None, 
    688        ) 
    689 
    690    def __pt_container__(self) -> Container: 
    691        return self.container 
    692 
    693 
    694_T = TypeVar("_T") 
    695 
    696 
    697class _DialogList(Generic[_T]): 
    698    """ 
    699    Common code for `RadioList` and `CheckboxList`. 
    700    """ 
    701 
    702    def __init__( 
    703        self, 
    704        values: Sequence[tuple[_T, AnyFormattedText]], 
    705        default_values: Sequence[_T] | None = None, 
    706        select_on_focus: bool = False, 
    707        open_character: str = "", 
    708        select_character: str = "*", 
    709        close_character: str = "", 
    710        container_style: str = "", 
    711        default_style: str = "", 
    712        number_style: str = "", 
    713        selected_style: str = "", 
    714        checked_style: str = "", 
    715        multiple_selection: bool = False, 
    716        show_scrollbar: bool = True, 
    717        show_cursor: bool = True, 
    718        show_numbers: bool = False, 
    719    ) -> None: 
    720        assert len(values) > 0 
    721        default_values = default_values or [] 
    722 
    723        self.values = values 
    724        self.show_numbers = show_numbers 
    725 
    726        self.open_character = open_character 
    727        self.select_character = select_character 
    728        self.close_character = close_character 
    729        self.container_style = container_style 
    730        self.default_style = default_style 
    731        self.number_style = number_style 
    732        self.selected_style = selected_style 
    733        self.checked_style = checked_style 
    734        self.multiple_selection = multiple_selection 
    735        self.show_scrollbar = show_scrollbar 
    736 
    737        # current_values will be used in multiple_selection, 
    738        # current_value will be used otherwise. 
    739        keys: list[_T] = [value for (value, _) in values] 
    740        self.current_values: list[_T] = [ 
    741            value for value in default_values if value in keys 
    742        ] 
    743        self.current_value: _T = ( 
    744            default_values[0] 
    745            if len(default_values) and default_values[0] in keys 
    746            else values[0][0] 
    747        ) 
    748 
    749        # Cursor index: take first selected item or first item otherwise. 
    750        if len(self.current_values) > 0: 
    751            self._selected_index = keys.index(self.current_values[0]) 
    752        else: 
    753            self._selected_index = 0 
    754 
    755        # Key bindings. 
    756        kb = KeyBindings() 
    757 
    758        @kb.add("up") 
    759        @kb.add("k")  # Vi-like. 
    760        def _up(event: E) -> None: 
    761            self._selected_index = max(0, self._selected_index - 1) 
    762            if select_on_focus: 
    763                self._handle_enter() 
    764 
    765        @kb.add("down") 
    766        @kb.add("j")  # Vi-like. 
    767        def _down(event: E) -> None: 
    768            self._selected_index = min(len(self.values) - 1, self._selected_index + 1) 
    769            if select_on_focus: 
    770                self._handle_enter() 
    771 
    772        @kb.add("pageup") 
    773        def _pageup(event: E) -> None: 
    774            w = event.app.layout.current_window 
    775            if w.render_info: 
    776                self._selected_index = max( 
    777                    0, self._selected_index - len(w.render_info.displayed_lines) 
    778                ) 
    779 
    780        @kb.add("pagedown") 
    781        def _pagedown(event: E) -> None: 
    782            w = event.app.layout.current_window 
    783            if w.render_info: 
    784                self._selected_index = min( 
    785                    len(self.values) - 1, 
    786                    self._selected_index + len(w.render_info.displayed_lines), 
    787                ) 
    788 
    789        @kb.add("enter") 
    790        @kb.add(" ") 
    791        def _click(event: E) -> None: 
    792            self._handle_enter() 
    793 
    794        @kb.add(Keys.Any) 
    795        def _find(event: E) -> None: 
    796            # We first check values after the selected value, then all values. 
    797            values = list(self.values) 
    798            for value in values[self._selected_index + 1 :] + values: 
    799                text = fragment_list_to_text(to_formatted_text(value[1])).lower() 
    800 
    801                if text.startswith(event.data.lower()): 
    802                    self._selected_index = self.values.index(value) 
    803                    return 
    804 
    805        numbers_visible = Condition(lambda: self.show_numbers) 
    806 
    807        for i in range(1, 10): 
    808 
    809            @kb.add(str(i), filter=numbers_visible) 
    810            def _select_i(event: E, index: int = i) -> None: 
    811                self._selected_index = min(len(self.values) - 1, index - 1) 
    812                if select_on_focus: 
    813                    self._handle_enter() 
    814 
    815        # Control and window. 
    816        self.control = FormattedTextControl( 
    817            self._get_text_fragments, 
    818            key_bindings=kb, 
    819            focusable=True, 
    820            show_cursor=show_cursor, 
    821        ) 
    822 
    823        self.window = Window( 
    824            content=self.control, 
    825            style=self.container_style, 
    826            right_margins=[ 
    827                ConditionalMargin( 
    828                    margin=ScrollbarMargin(display_arrows=True), 
    829                    filter=Condition(lambda: self.show_scrollbar), 
    830                ), 
    831            ], 
    832            dont_extend_height=True, 
    833        ) 
    834 
    835    def _handle_enter(self) -> None: 
    836        if self.multiple_selection: 
    837            val = self.values[self._selected_index][0] 
    838            if val in self.current_values: 
    839                self.current_values.remove(val) 
    840            else: 
    841                self.current_values.append(val) 
    842        else: 
    843            self.current_value = self.values[self._selected_index][0] 
    844 
    845    def _get_text_fragments(self) -> StyleAndTextTuples: 
    846        def mouse_handler(mouse_event: MouseEvent) -> None: 
    847            """ 
    848            Set `_selected_index` and `current_value` according to the y 
    849            position of the mouse click event. 
    850            """ 
    851            if mouse_event.event_type == MouseEventType.MOUSE_UP: 
    852                self._selected_index = mouse_event.position.y 
    853                self._handle_enter() 
    854 
    855        result: StyleAndTextTuples = [] 
    856        for i, value in enumerate(self.values): 
    857            if self.multiple_selection: 
    858                checked = value[0] in self.current_values 
    859            else: 
    860                checked = value[0] == self.current_value 
    861            selected = i == self._selected_index 
    862 
    863            style = "" 
    864            if checked: 
    865                style += " " + self.checked_style 
    866            if selected: 
    867                style += " " + self.selected_style 
    868 
    869            result.append((style, self.open_character)) 
    870 
    871            if selected: 
    872                result.append(("[SetCursorPosition]", "")) 
    873 
    874            if checked: 
    875                result.append((style, self.select_character)) 
    876            else: 
    877                result.append((style, " ")) 
    878 
    879            result.append((style, self.close_character)) 
    880            result.append((f"{style} {self.default_style}", " ")) 
    881 
    882            if self.show_numbers: 
    883                result.append((f"{style} {self.number_style}", f"{i + 1:2d}. ")) 
    884 
    885            result.extend( 
    886                to_formatted_text(value[1], style=f"{style} {self.default_style}") 
    887            ) 
    888            result.append(("", "\n")) 
    889 
    890        # Add mouse handler to all fragments. 
    891        for i in range(len(result)): 
    892            result[i] = (result[i][0], result[i][1], mouse_handler) 
    893 
    894        result.pop()  # Remove last newline. 
    895        return result 
    896 
    897    def __pt_container__(self) -> Container: 
    898        return self.window 
    899 
    900 
    901class RadioList(_DialogList[_T]): 
    902    """ 
    903    List of radio buttons. Only one can be checked at the same time. 
    904 
    905    :param values: List of (value, label) tuples. 
    906    """ 
    907 
    908    def __init__( 
    909        self, 
    910        values: Sequence[tuple[_T, AnyFormattedText]], 
    911        default: _T | None = None, 
    912        show_numbers: bool = False, 
    913        select_on_focus: bool = False, 
    914        open_character: str = "(", 
    915        select_character: str = "*", 
    916        close_character: str = ")", 
    917        container_style: str = "class:radio-list", 
    918        default_style: str = "class:radio", 
    919        selected_style: str = "class:radio-selected", 
    920        checked_style: str = "class:radio-checked", 
    921        number_style: str = "class:radio-number", 
    922        multiple_selection: bool = False, 
    923        show_cursor: bool = True, 
    924        show_scrollbar: bool = True, 
    925    ) -> None: 
    926        if default is None: 
    927            default_values = None 
    928        else: 
    929            default_values = [default] 
    930 
    931        super().__init__( 
    932            values, 
    933            default_values=default_values, 
    934            select_on_focus=select_on_focus, 
    935            show_numbers=show_numbers, 
    936            open_character=open_character, 
    937            select_character=select_character, 
    938            close_character=close_character, 
    939            container_style=container_style, 
    940            default_style=default_style, 
    941            selected_style=selected_style, 
    942            checked_style=checked_style, 
    943            number_style=number_style, 
    944            multiple_selection=False, 
    945            show_cursor=show_cursor, 
    946            show_scrollbar=show_scrollbar, 
    947        ) 
    948 
    949 
    950class CheckboxList(_DialogList[_T]): 
    951    """ 
    952    List of checkbox buttons. Several can be checked at the same time. 
    953 
    954    :param values: List of (value, label) tuples. 
    955    """ 
    956 
    957    def __init__( 
    958        self, 
    959        values: Sequence[tuple[_T, AnyFormattedText]], 
    960        default_values: Sequence[_T] | None = None, 
    961        open_character: str = "[", 
    962        select_character: str = "*", 
    963        close_character: str = "]", 
    964        container_style: str = "class:checkbox-list", 
    965        default_style: str = "class:checkbox", 
    966        selected_style: str = "class:checkbox-selected", 
    967        checked_style: str = "class:checkbox-checked", 
    968    ) -> None: 
    969        super().__init__( 
    970            values, 
    971            default_values=default_values, 
    972            open_character=open_character, 
    973            select_character=select_character, 
    974            close_character=close_character, 
    975            container_style=container_style, 
    976            default_style=default_style, 
    977            selected_style=selected_style, 
    978            checked_style=checked_style, 
    979            multiple_selection=True, 
    980        ) 
    981 
    982 
    983class Checkbox(CheckboxList[str]): 
    984    """Backward compatibility util: creates a 1-sized CheckboxList 
    985 
    986    :param text: the text 
    987    """ 
    988 
    989    show_scrollbar = False 
    990 
    991    def __init__(self, text: AnyFormattedText = "", checked: bool = False) -> None: 
    992        values = [("value", text)] 
    993        super().__init__(values=values) 
    994        self.checked = checked 
    995 
    996    @property 
    997    def checked(self) -> bool: 
    998        return "value" in self.current_values 
    999 
    1000    @checked.setter 
    1001    def checked(self, value: bool) -> None: 
    1002        if value: 
    1003            self.current_values = ["value"] 
    1004        else: 
    1005            self.current_values = [] 
    1006 
    1007 
    1008class VerticalLine: 
    1009    """ 
    1010    A simple vertical line with a width of 1. 
    1011    """ 
    1012 
    1013    def __init__(self) -> None: 
    1014        self.window = Window( 
    1015            char=Border.VERTICAL, style="class:line,vertical-line", width=1 
    1016        ) 
    1017 
    1018    def __pt_container__(self) -> Container: 
    1019        return self.window 
    1020 
    1021 
    1022class HorizontalLine: 
    1023    """ 
    1024    A simple horizontal line with a height of 1. 
    1025    """ 
    1026 
    1027    def __init__(self) -> None: 
    1028        self.window = Window( 
    1029            char=Border.HORIZONTAL, style="class:line,horizontal-line", height=1 
    1030        ) 
    1031 
    1032    def __pt_container__(self) -> Container: 
    1033        return self.window 
    1034 
    1035 
    1036class ProgressBar: 
    1037    def __init__(self) -> None: 
    1038        self._percentage = 60 
    1039 
    1040        self.label = Label("60%") 
    1041        self.container = FloatContainer( 
    1042            content=Window(height=1), 
    1043            floats=[ 
    1044                # We first draw the label, then the actual progress bar.  Right 
    1045                # now, this is the only way to have the colors of the progress 
    1046                # bar appear on top of the label. The problem is that our label 
    1047                # can't be part of any `Window` below. 
    1048                Float(content=self.label, top=0, bottom=0), 
    1049                Float( 
    1050                    left=0, 
    1051                    top=0, 
    1052                    right=0, 
    1053                    bottom=0, 
    1054                    content=VSplit( 
    1055                        [ 
    1056                            Window( 
    1057                                style="class:progress-bar.used", 
    1058                                width=lambda: D(weight=int(self._percentage)), 
    1059                            ), 
    1060                            Window( 
    1061                                style="class:progress-bar", 
    1062                                width=lambda: D(weight=int(100 - self._percentage)), 
    1063                            ), 
    1064                        ] 
    1065                    ), 
    1066                ), 
    1067            ], 
    1068        ) 
    1069 
    1070    @property 
    1071    def percentage(self) -> int: 
    1072        return self._percentage 
    1073 
    1074    @percentage.setter 
    1075    def percentage(self, value: int) -> None: 
    1076        self._percentage = value 
    1077        self.label.text = f"{value}%" 
    1078 
    1079    def __pt_container__(self) -> Container: 
    1080        return self.container