diff options
| author | Devtools Arcadia <[email protected]> | 2022-02-07 18:08:42 +0300 | 
|---|---|---|
| committer | Devtools Arcadia <[email protected]> | 2022-02-07 18:08:42 +0300 | 
| commit | 1110808a9d39d4b808aef724c861a2e1a38d2a69 (patch) | |
| tree | e26c9fed0de5d9873cce7e00bc214573dc2195b7 /contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets | |
intermediate changes
ref:cde9a383711a11544ce7e107a78147fb96cc4029
Diffstat (limited to 'contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets')
5 files changed, 1861 insertions, 0 deletions
| diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/__init__.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/__init__.py new file mode 100644 index 00000000000..552d3559488 --- /dev/null +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/__init__.py @@ -0,0 +1,60 @@ +""" +Collection of reusable components for building full screen applications. +These are higher level abstractions on top of the `prompt_toolkit.layout` +module. + +Most of these widgets implement the ``__pt_container__`` method, which makes it +possible to embed these in the layout like any other container. +""" +from .base import ( +    Box, +    Button, +    Checkbox, +    CheckboxList, +    Frame, +    HorizontalLine, +    Label, +    ProgressBar, +    RadioList, +    Shadow, +    TextArea, +    VerticalLine, +) +from .dialogs import Dialog +from .menus import MenuContainer, MenuItem +from .toolbars import ( +    ArgToolbar, +    CompletionsToolbar, +    FormattedTextToolbar, +    SearchToolbar, +    SystemToolbar, +    ValidationToolbar, +) + +__all__ = [ +    # Base. +    "TextArea", +    "Label", +    "Button", +    "Frame", +    "Shadow", +    "Box", +    "VerticalLine", +    "HorizontalLine", +    "CheckboxList", +    "RadioList", +    "Checkbox", +    "ProgressBar", +    # Toolbars. +    "ArgToolbar", +    "CompletionsToolbar", +    "FormattedTextToolbar", +    "SearchToolbar", +    "SystemToolbar", +    "ValidationToolbar", +    # Dialogs. +    "Dialog", +    # Menus. +    "MenuContainer", +    "MenuItem", +] diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/base.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/base.py new file mode 100644 index 00000000000..bb35a2c62ea --- /dev/null +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/base.py @@ -0,0 +1,947 @@ +""" +Collection of reusable components for building full screen applications. + +All of these widgets implement the ``__pt_container__`` method, which makes +them usable in any situation where we are expecting a `prompt_toolkit` +container object. + +.. warning:: + +    At this point, the API for these widgets is considered unstable, and can +    potentially change between minor releases (we try not too, but no +    guarantees are made yet). The public API in +    `prompt_toolkit.shortcuts.dialogs` on the other hand is considered stable. +""" +from functools import partial +from typing import Callable, Generic, List, Optional, Sequence, Tuple, TypeVar, Union + +from prompt_toolkit.application.current import get_app +from prompt_toolkit.auto_suggest import AutoSuggest, DynamicAutoSuggest +from prompt_toolkit.buffer import Buffer, BufferAcceptHandler +from prompt_toolkit.completion import Completer, DynamicCompleter +from prompt_toolkit.document import Document +from prompt_toolkit.filters import ( +    Condition, +    FilterOrBool, +    has_focus, +    is_done, +    is_true, +    to_filter, +) +from prompt_toolkit.formatted_text import ( +    AnyFormattedText, +    StyleAndTextTuples, +    Template, +    to_formatted_text, +) +from prompt_toolkit.formatted_text.utils import fragment_list_to_text +from prompt_toolkit.history import History +from prompt_toolkit.key_binding.key_bindings import KeyBindings +from prompt_toolkit.key_binding.key_processor import KeyPressEvent +from prompt_toolkit.keys import Keys +from prompt_toolkit.layout.containers import ( +    AnyContainer, +    ConditionalContainer, +    Container, +    DynamicContainer, +    Float, +    FloatContainer, +    HSplit, +    VSplit, +    Window, +    WindowAlign, +) +from prompt_toolkit.layout.controls import ( +    BufferControl, +    FormattedTextControl, +    GetLinePrefixCallable, +) +from prompt_toolkit.layout.dimension import AnyDimension +from prompt_toolkit.layout.dimension import Dimension as D +from prompt_toolkit.layout.dimension import to_dimension +from prompt_toolkit.layout.margins import ( +    ConditionalMargin, +    NumberedMargin, +    ScrollbarMargin, +) +from prompt_toolkit.layout.processors import ( +    AppendAutoSuggestion, +    BeforeInput, +    ConditionalProcessor, +    PasswordProcessor, +    Processor, +) +from prompt_toolkit.lexers import DynamicLexer, Lexer +from prompt_toolkit.mouse_events import MouseEvent, MouseEventType +from prompt_toolkit.utils import get_cwidth +from prompt_toolkit.validation import DynamicValidator, Validator + +from .toolbars import SearchToolbar + +__all__ = [ +    "TextArea", +    "Label", +    "Button", +    "Frame", +    "Shadow", +    "Box", +    "VerticalLine", +    "HorizontalLine", +    "RadioList", +    "CheckboxList", +    "Checkbox",  # backward compatibility +    "ProgressBar", +] + +E = KeyPressEvent + + +class Border: +    "Box drawing characters. (Thin)" +    HORIZONTAL = "\u2500" +    VERTICAL = "\u2502" +    TOP_LEFT = "\u250c" +    TOP_RIGHT = "\u2510" +    BOTTOM_LEFT = "\u2514" +    BOTTOM_RIGHT = "\u2518" + + +class TextArea: +    """ +    A simple input field. + +    This is a higher level abstraction on top of several other classes with +    sane defaults. + +    This widget does have the most common options, but it does not intend to +    cover every single use case. For more configurations options, you can +    always build a text area manually, using a +    :class:`~prompt_toolkit.buffer.Buffer`, +    :class:`~prompt_toolkit.layout.BufferControl` and +    :class:`~prompt_toolkit.layout.Window`. + +    Buffer attributes: + +    :param text: The initial text. +    :param multiline: If True, allow multiline input. +    :param completer: :class:`~prompt_toolkit.completion.Completer` instance +        for auto completion. +    :param complete_while_typing: Boolean. +    :param accept_handler: Called when `Enter` is pressed (This should be a +        callable that takes a buffer as input). +    :param history: :class:`~prompt_toolkit.history.History` instance. +    :param auto_suggest: :class:`~prompt_toolkit.auto_suggest.AutoSuggest` +        instance for input suggestions. + +    BufferControl attributes: + +    :param password: When `True`, display using asterisks. +    :param focusable: When `True`, allow this widget to receive the focus. +    :param focus_on_click: When `True`, focus after mouse click. +    :param input_processors: `None` or a list of +        :class:`~prompt_toolkit.layout.Processor` objects. +    :param validator: `None` or a :class:`~prompt_toolkit.validation.Validator` +        object. + +    Window attributes: + +    :param lexer: :class:`~prompt_toolkit.lexers.Lexer` instance for syntax +        highlighting. +    :param wrap_lines: When `True`, don't scroll horizontally, but wrap lines. +    :param width: Window width. (:class:`~prompt_toolkit.layout.Dimension` object.) +    :param height: Window height. (:class:`~prompt_toolkit.layout.Dimension` object.) +    :param scrollbar: When `True`, display a scroll bar. +    :param style: A style string. +    :param dont_extend_width: When `True`, don't take up more width then the +                              preferred width reported by the control. +    :param dont_extend_height: When `True`, don't take up more width then the +                               preferred height reported by the control. +    :param get_line_prefix: None or a callable that returns formatted text to +        be inserted before a line. It takes a line number (int) and a +        wrap_count and returns formatted text. This can be used for +        implementation of line continuations, things like Vim "breakindent" and +        so on. + +    Other attributes: + +    :param search_field: An optional `SearchToolbar` object. +    """ + +    def __init__( +        self, +        text: str = "", +        multiline: FilterOrBool = True, +        password: FilterOrBool = False, +        lexer: Optional[Lexer] = None, +        auto_suggest: Optional[AutoSuggest] = None, +        completer: Optional[Completer] = None, +        complete_while_typing: FilterOrBool = True, +        validator: Optional[Validator] = None, +        accept_handler: Optional[BufferAcceptHandler] = None, +        history: Optional[History] = None, +        focusable: FilterOrBool = True, +        focus_on_click: FilterOrBool = False, +        wrap_lines: FilterOrBool = True, +        read_only: FilterOrBool = False, +        width: AnyDimension = None, +        height: AnyDimension = None, +        dont_extend_height: FilterOrBool = False, +        dont_extend_width: FilterOrBool = False, +        line_numbers: bool = False, +        get_line_prefix: Optional[GetLinePrefixCallable] = None, +        scrollbar: bool = False, +        style: str = "", +        search_field: Optional[SearchToolbar] = None, +        preview_search: FilterOrBool = True, +        prompt: AnyFormattedText = "", +        input_processors: Optional[List[Processor]] = None, +    ) -> None: + +        if search_field is None: +            search_control = None +        elif isinstance(search_field, SearchToolbar): +            search_control = search_field.control + +        if input_processors is None: +            input_processors = [] + +        # Writeable attributes. +        self.completer = completer +        self.complete_while_typing = complete_while_typing +        self.lexer = lexer +        self.auto_suggest = auto_suggest +        self.read_only = read_only +        self.wrap_lines = wrap_lines +        self.validator = validator + +        self.buffer = Buffer( +            document=Document(text, 0), +            multiline=multiline, +            read_only=Condition(lambda: is_true(self.read_only)), +            completer=DynamicCompleter(lambda: self.completer), +            complete_while_typing=Condition( +                lambda: is_true(self.complete_while_typing) +            ), +            validator=DynamicValidator(lambda: self.validator), +            auto_suggest=DynamicAutoSuggest(lambda: self.auto_suggest), +            accept_handler=accept_handler, +            history=history, +        ) + +        self.control = BufferControl( +            buffer=self.buffer, +            lexer=DynamicLexer(lambda: self.lexer), +            input_processors=[ +                ConditionalProcessor( +                    AppendAutoSuggestion(), has_focus(self.buffer) & ~is_done +                ), +                ConditionalProcessor( +                    processor=PasswordProcessor(), filter=to_filter(password) +                ), +                BeforeInput(prompt, style="class:text-area.prompt"), +            ] +            + input_processors, +            search_buffer_control=search_control, +            preview_search=preview_search, +            focusable=focusable, +            focus_on_click=focus_on_click, +        ) + +        if multiline: +            if scrollbar: +                right_margins = [ScrollbarMargin(display_arrows=True)] +            else: +                right_margins = [] +            if line_numbers: +                left_margins = [NumberedMargin()] +            else: +                left_margins = [] +        else: +            height = D.exact(1) +            left_margins = [] +            right_margins = [] + +        style = "class:text-area " + style + +        # If no height was given, guarantee height of at least 1. +        if height is None: +            height = D(min=1) + +        self.window = Window( +            height=height, +            width=width, +            dont_extend_height=dont_extend_height, +            dont_extend_width=dont_extend_width, +            content=self.control, +            style=style, +            wrap_lines=Condition(lambda: is_true(self.wrap_lines)), +            left_margins=left_margins, +            right_margins=right_margins, +            get_line_prefix=get_line_prefix, +        ) + +    @property +    def text(self) -> str: +        """ +        The `Buffer` text. +        """ +        return self.buffer.text + +    @text.setter +    def text(self, value: str) -> None: +        self.document = Document(value, 0) + +    @property +    def document(self) -> Document: +        """ +        The `Buffer` document (text + cursor position). +        """ +        return self.buffer.document + +    @document.setter +    def document(self, value: Document) -> None: +        self.buffer.set_document(value, bypass_readonly=True) + +    @property +    def accept_handler(self) -> Optional[BufferAcceptHandler]: +        """ +        The accept handler. Called when the user accepts the input. +        """ +        return self.buffer.accept_handler + +    @accept_handler.setter +    def accept_handler(self, value: BufferAcceptHandler) -> None: +        self.buffer.accept_handler = value + +    def __pt_container__(self) -> Container: +        return self.window + + +class Label: +    """ +    Widget that displays the given text. It is not editable or focusable. + +    :param text: Text to display. Can be multiline. All value types accepted by +        :class:`prompt_toolkit.layout.FormattedTextControl` are allowed, +        including a callable. +    :param style: A style string. +    :param width: When given, use this width, rather than calculating it from +        the text size. +    :param dont_extend_width: When `True`, don't take up more width than +                              preferred, i.e. the length of the longest line of +                              the text, or value of `width` parameter, if +                              given. `True` by default +    :param dont_extend_height: When `True`, don't take up more width than the +                               preferred height, i.e. the number of lines of +                               the text. `False` by default. +    """ + +    def __init__( +        self, +        text: AnyFormattedText, +        style: str = "", +        width: AnyDimension = None, +        dont_extend_height: bool = True, +        dont_extend_width: bool = False, +    ) -> None: + +        self.text = text + +        def get_width() -> AnyDimension: +            if width is None: +                text_fragments = to_formatted_text(self.text) +                text = fragment_list_to_text(text_fragments) +                if text: +                    longest_line = max(get_cwidth(line) for line in text.splitlines()) +                else: +                    return D(preferred=0) +                return D(preferred=longest_line) +            else: +                return width + +        self.formatted_text_control = FormattedTextControl(text=lambda: self.text) + +        self.window = Window( +            content=self.formatted_text_control, +            width=get_width, +            height=D(min=1), +            style="class:label " + style, +            dont_extend_height=dont_extend_height, +            dont_extend_width=dont_extend_width, +        ) + +    def __pt_container__(self) -> Container: +        return self.window + + +class Button: +    """ +    Clickable button. + +    :param text: The caption for the button. +    :param handler: `None` or callable. Called when the button is clicked. No +        parameters are passed to this callable. Use for instance Python's +        `functools.partial` to pass parameters to this callable if needed. +    :param width: Width of the button. +    """ + +    def __init__( +        self, +        text: str, +        handler: Optional[Callable[[], None]] = None, +        width: int = 12, +        left_symbol: str = "<", +        right_symbol: str = ">", +    ) -> None: + +        self.text = text +        self.left_symbol = left_symbol +        self.right_symbol = right_symbol +        self.handler = handler +        self.width = width +        self.control = FormattedTextControl( +            self._get_text_fragments, +            key_bindings=self._get_key_bindings(), +            focusable=True, +        ) + +        def get_style() -> str: +            if get_app().layout.has_focus(self): +                return "class:button.focused" +            else: +                return "class:button" + +        # Note: `dont_extend_width` is False, because we want to allow buttons +        #       to take more space if the parent container provides more space. +        #       Otherwise, we will also truncate the text. +        #       Probably we need a better way here to adjust to width of the +        #       button to the text. + +        self.window = Window( +            self.control, +            align=WindowAlign.CENTER, +            height=1, +            width=width, +            style=get_style, +            dont_extend_width=False, +            dont_extend_height=True, +        ) + +    def _get_text_fragments(self) -> StyleAndTextTuples: +        width = self.width - ( +            get_cwidth(self.left_symbol) + get_cwidth(self.right_symbol) +        ) +        text = ("{{:^{}}}".format(width)).format(self.text) + +        def handler(mouse_event: MouseEvent) -> None: +            if ( +                self.handler is not None +                and mouse_event.event_type == MouseEventType.MOUSE_UP +            ): +                self.handler() + +        return [ +            ("class:button.arrow", self.left_symbol, handler), +            ("[SetCursorPosition]", ""), +            ("class:button.text", text, handler), +            ("class:button.arrow", self.right_symbol, handler), +        ] + +    def _get_key_bindings(self) -> KeyBindings: +        "Key bindings for the Button." +        kb = KeyBindings() + +        @kb.add(" ") +        @kb.add("enter") +        def _(event: E) -> None: +            if self.handler is not None: +                self.handler() + +        return kb + +    def __pt_container__(self) -> Container: +        return self.window + + +class Frame: +    """ +    Draw a border around any container, optionally with a title text. + +    Changing the title and body of the frame is possible at runtime by +    assigning to the `body` and `title` attributes of this class. + +    :param body: Another container object. +    :param title: Text to be displayed in the top of the frame (can be formatted text). +    :param style: Style string to be applied to this widget. +    """ + +    def __init__( +        self, +        body: AnyContainer, +        title: AnyFormattedText = "", +        style: str = "", +        width: AnyDimension = None, +        height: AnyDimension = None, +        key_bindings: Optional[KeyBindings] = None, +        modal: bool = False, +    ) -> None: + +        self.title = title +        self.body = body + +        fill = partial(Window, style="class:frame.border") +        style = "class:frame " + style + +        top_row_with_title = VSplit( +            [ +                fill(width=1, height=1, char=Border.TOP_LEFT), +                fill(char=Border.HORIZONTAL), +                fill(width=1, height=1, char="|"), +                # Notice: we use `Template` here, because `self.title` can be an +                # `HTML` object for instance. +                Label( +                    lambda: Template(" {} ").format(self.title), +                    style="class:frame.label", +                    dont_extend_width=True, +                ), +                fill(width=1, height=1, char="|"), +                fill(char=Border.HORIZONTAL), +                fill(width=1, height=1, char=Border.TOP_RIGHT), +            ], +            height=1, +        ) + +        top_row_without_title = VSplit( +            [ +                fill(width=1, height=1, char=Border.TOP_LEFT), +                fill(char=Border.HORIZONTAL), +                fill(width=1, height=1, char=Border.TOP_RIGHT), +            ], +            height=1, +        ) + +        @Condition +        def has_title() -> bool: +            return bool(self.title) + +        self.container = HSplit( +            [ +                ConditionalContainer(content=top_row_with_title, filter=has_title), +                ConditionalContainer(content=top_row_without_title, filter=~has_title), +                VSplit( +                    [ +                        fill(width=1, char=Border.VERTICAL), +                        DynamicContainer(lambda: self.body), +                        fill(width=1, char=Border.VERTICAL), +                        # Padding is required to make sure that if the content is +                        # too small, the right frame border is still aligned. +                    ], +                    padding=0, +                ), +                VSplit( +                    [ +                        fill(width=1, height=1, char=Border.BOTTOM_LEFT), +                        fill(char=Border.HORIZONTAL), +                        fill(width=1, height=1, char=Border.BOTTOM_RIGHT), +                    ], +                    # specifying height here will increase the rendering speed. +                    height=1, +                ), +            ], +            width=width, +            height=height, +            style=style, +            key_bindings=key_bindings, +            modal=modal, +        ) + +    def __pt_container__(self) -> Container: +        return self.container + + +class Shadow: +    """ +    Draw a shadow underneath/behind this container. +    (This applies `class:shadow` the the cells under the shadow. The Style +    should define the colors for the shadow.) + +    :param body: Another container object. +    """ + +    def __init__(self, body: AnyContainer) -> None: +        self.container = FloatContainer( +            content=body, +            floats=[ +                Float( +                    bottom=-1, +                    height=1, +                    left=1, +                    right=-1, +                    transparent=True, +                    content=Window(style="class:shadow"), +                ), +                Float( +                    bottom=-1, +                    top=1, +                    width=1, +                    right=-1, +                    transparent=True, +                    content=Window(style="class:shadow"), +                ), +            ], +        ) + +    def __pt_container__(self) -> Container: +        return self.container + + +class Box: +    """ +    Add padding around a container. + +    This also makes sure that the parent can provide more space than required by +    the child. This is very useful when wrapping a small element with a fixed +    size into a ``VSplit`` or ``HSplit`` object. The ``HSplit`` and ``VSplit`` +    try to make sure to adapt respectively the width and height, possibly +    shrinking other elements. Wrapping something in a ``Box`` makes it flexible. + +    :param body: Another container object. +    :param padding: The margin to be used around the body. This can be +        overridden by `padding_left`, padding_right`, `padding_top` and +        `padding_bottom`. +    :param style: A style string. +    :param char: Character to be used for filling the space around the body. +        (This is supposed to be a character with a terminal width of 1.) +    """ + +    def __init__( +        self, +        body: AnyContainer, +        padding: AnyDimension = None, +        padding_left: AnyDimension = None, +        padding_right: AnyDimension = None, +        padding_top: AnyDimension = None, +        padding_bottom: AnyDimension = None, +        width: AnyDimension = None, +        height: AnyDimension = None, +        style: str = "", +        char: Union[None, str, Callable[[], str]] = None, +        modal: bool = False, +        key_bindings: Optional[KeyBindings] = None, +    ) -> None: + +        if padding is None: +            padding = D(preferred=0) + +        def get(value: AnyDimension) -> D: +            if value is None: +                value = padding +            return to_dimension(value) + +        self.padding_left = get(padding_left) +        self.padding_right = get(padding_right) +        self.padding_top = get(padding_top) +        self.padding_bottom = get(padding_bottom) +        self.body = body + +        self.container = HSplit( +            [ +                Window(height=self.padding_top, char=char), +                VSplit( +                    [ +                        Window(width=self.padding_left, char=char), +                        body, +                        Window(width=self.padding_right, char=char), +                    ] +                ), +                Window(height=self.padding_bottom, char=char), +            ], +            width=width, +            height=height, +            style=style, +            modal=modal, +            key_bindings=None, +        ) + +    def __pt_container__(self) -> Container: +        return self.container + + +_T = TypeVar("_T") + + +class _DialogList(Generic[_T]): +    """ +    Common code for `RadioList` and `CheckboxList`. +    """ + +    open_character: str = "" +    close_character: str = "" +    container_style: str = "" +    default_style: str = "" +    selected_style: str = "" +    checked_style: str = "" +    multiple_selection: bool = False +    show_scrollbar: bool = True + +    def __init__(self, values: Sequence[Tuple[_T, AnyFormattedText]]) -> None: +        assert len(values) > 0 + +        self.values = values +        # current_values will be used in multiple_selection, +        # current_value will be used otherwise. +        self.current_values: List[_T] = [] +        self.current_value: _T = values[0][0] +        self._selected_index = 0 + +        # Key bindings. +        kb = KeyBindings() + +        @kb.add("up") +        def _up(event: E) -> None: +            self._selected_index = max(0, self._selected_index - 1) + +        @kb.add("down") +        def _down(event: E) -> None: +            self._selected_index = min(len(self.values) - 1, self._selected_index + 1) + +        @kb.add("pageup") +        def _pageup(event: E) -> None: +            w = event.app.layout.current_window +            if w.render_info: +                self._selected_index = max( +                    0, self._selected_index - len(w.render_info.displayed_lines) +                ) + +        @kb.add("pagedown") +        def _pagedown(event: E) -> None: +            w = event.app.layout.current_window +            if w.render_info: +                self._selected_index = min( +                    len(self.values) - 1, +                    self._selected_index + len(w.render_info.displayed_lines), +                ) + +        @kb.add("enter") +        @kb.add(" ") +        def _click(event: E) -> None: +            self._handle_enter() + +        @kb.add(Keys.Any) +        def _find(event: E) -> None: +            # We first check values after the selected value, then all values. +            values = list(self.values) +            for value in values[self._selected_index + 1 :] + values: +                text = fragment_list_to_text(to_formatted_text(value[1])).lower() + +                if text.startswith(event.data.lower()): +                    self._selected_index = self.values.index(value) +                    return + +        # Control and window. +        self.control = FormattedTextControl( +            self._get_text_fragments, key_bindings=kb, focusable=True +        ) + +        self.window = Window( +            content=self.control, +            style=self.container_style, +            right_margins=[ +                ConditionalMargin( +                    margin=ScrollbarMargin(display_arrows=True), +                    filter=Condition(lambda: self.show_scrollbar), +                ), +            ], +            dont_extend_height=True, +        ) + +    def _handle_enter(self) -> None: +        if self.multiple_selection: +            val = self.values[self._selected_index][0] +            if val in self.current_values: +                self.current_values.remove(val) +            else: +                self.current_values.append(val) +        else: +            self.current_value = self.values[self._selected_index][0] + +    def _get_text_fragments(self) -> StyleAndTextTuples: +        def mouse_handler(mouse_event: MouseEvent) -> None: +            """ +            Set `_selected_index` and `current_value` according to the y +            position of the mouse click event. +            """ +            if mouse_event.event_type == MouseEventType.MOUSE_UP: +                self._selected_index = mouse_event.position.y +                self._handle_enter() + +        result: StyleAndTextTuples = [] +        for i, value in enumerate(self.values): +            if self.multiple_selection: +                checked = value[0] in self.current_values +            else: +                checked = value[0] == self.current_value +            selected = i == self._selected_index + +            style = "" +            if checked: +                style += " " + self.checked_style +            if selected: +                style += " " + self.selected_style + +            result.append((style, self.open_character)) + +            if selected: +                result.append(("[SetCursorPosition]", "")) + +            if checked: +                result.append((style, "*")) +            else: +                result.append((style, " ")) + +            result.append((style, self.close_character)) +            result.append((self.default_style, " ")) +            result.extend(to_formatted_text(value[1], style=self.default_style)) +            result.append(("", "\n")) + +        # Add mouse handler to all fragments. +        for i in range(len(result)): +            result[i] = (result[i][0], result[i][1], mouse_handler) + +        result.pop()  # Remove last newline. +        return result + +    def __pt_container__(self) -> Container: +        return self.window + + +class RadioList(_DialogList[_T]): +    """ +    List of radio buttons. Only one can be checked at the same time. + +    :param values: List of (value, label) tuples. +    """ + +    open_character = "(" +    close_character = ")" +    container_style = "class:radio-list" +    default_style = "class:radio" +    selected_style = "class:radio-selected" +    checked_style = "class:radio-checked" +    multiple_selection = False + + +class CheckboxList(_DialogList[_T]): +    """ +    List of checkbox buttons. Several can be checked at the same time. + +    :param values: List of (value, label) tuples. +    """ + +    open_character = "[" +    close_character = "]" +    container_style = "class:checkbox-list" +    default_style = "class:checkbox" +    selected_style = "class:checkbox-selected" +    checked_style = "class:checkbox-checked" +    multiple_selection = True + + +class Checkbox(CheckboxList[str]): +    """Backward compatibility util: creates a 1-sized CheckboxList + +    :param text: the text +    """ + +    show_scrollbar = False + +    def __init__(self, text: AnyFormattedText = "", checked: bool = False) -> None: +        values = [("value", text)] +        CheckboxList.__init__(self, values) +        self.checked = checked + +    @property +    def checked(self) -> bool: +        return "value" in self.current_values + +    @checked.setter +    def checked(self, value: bool) -> None: +        if value: +            self.current_values = ["value"] +        else: +            self.current_values = [] + + +class VerticalLine(object): +    """ +    A simple vertical line with a width of 1. +    """ + +    def __init__(self) -> None: +        self.window = Window( +            char=Border.VERTICAL, style="class:line,vertical-line", width=1 +        ) + +    def __pt_container__(self) -> Container: +        return self.window + + +class HorizontalLine: +    """ +    A simple horizontal line with a height of 1. +    """ + +    def __init__(self) -> None: +        self.window = Window( +            char=Border.HORIZONTAL, style="class:line,horizontal-line", height=1 +        ) + +    def __pt_container__(self) -> Container: +        return self.window + + +class ProgressBar: +    def __init__(self) -> None: +        self._percentage = 60 + +        self.label = Label("60%") +        self.container = FloatContainer( +            content=Window(height=1), +            floats=[ +                # We first draw the label, then the actual progress bar.  Right +                # now, this is the only way to have the colors of the progress +                # bar appear on top of the label. The problem is that our label +                # can't be part of any `Window` below. +                Float(content=self.label, top=0, bottom=0), +                Float( +                    left=0, +                    top=0, +                    right=0, +                    bottom=0, +                    content=VSplit( +                        [ +                            Window( +                                style="class:progress-bar.used", +                                width=lambda: D(weight=int(self._percentage)), +                            ), +                            Window( +                                style="class:progress-bar", +                                width=lambda: D(weight=int(100 - self._percentage)), +                            ), +                        ] +                    ), +                ), +            ], +        ) + +    @property +    def percentage(self) -> int: +        return self._percentage + +    @percentage.setter +    def percentage(self, value: int) -> None: +        self._percentage = value +        self.label.text = "{0}%".format(value) + +    def __pt_container__(self) -> Container: +        return self.container diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/dialogs.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/dialogs.py new file mode 100644 index 00000000000..920582b4e68 --- /dev/null +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/dialogs.py @@ -0,0 +1,106 @@ +""" +Collection of reusable components for building full screen applications. +""" +from typing import Optional, Sequence, Union + +from prompt_toolkit.filters import has_completions, has_focus +from prompt_toolkit.formatted_text import AnyFormattedText +from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous +from prompt_toolkit.key_binding.key_bindings import KeyBindings +from prompt_toolkit.layout.containers import ( +    AnyContainer, +    DynamicContainer, +    HSplit, +    VSplit, +) +from prompt_toolkit.layout.dimension import AnyDimension +from prompt_toolkit.layout.dimension import Dimension as D + +from .base import Box, Button, Frame, Shadow + +__all__ = [ +    "Dialog", +] + + +class Dialog: +    """ +    Simple dialog window. This is the base for input dialogs, message dialogs +    and confirmation dialogs. + +    Changing the title and body of the dialog is possible at runtime by +    assigning to the `body` and `title` attributes of this class. + +    :param body: Child container object. +    :param title: Text to be displayed in the heading of the dialog. +    :param buttons: A list of `Button` widgets, displayed at the bottom. +    """ + +    def __init__( +        self, +        body: AnyContainer, +        title: AnyFormattedText = "", +        buttons: Optional[Sequence[Button]] = None, +        modal: bool = True, +        width: AnyDimension = None, +        with_background: bool = False, +    ) -> None: + +        self.body = body +        self.title = title + +        buttons = buttons or [] + +        # When a button is selected, handle left/right key bindings. +        buttons_kb = KeyBindings() +        if len(buttons) > 1: +            first_selected = has_focus(buttons[0]) +            last_selected = has_focus(buttons[-1]) + +            buttons_kb.add("left", filter=~first_selected)(focus_previous) +            buttons_kb.add("right", filter=~last_selected)(focus_next) + +        frame_body: AnyContainer +        if buttons: +            frame_body = HSplit( +                [ +                    # Add optional padding around the body. +                    Box( +                        body=DynamicContainer(lambda: self.body), +                        padding=D(preferred=1, max=1), +                        padding_bottom=0, +                    ), +                    # The buttons. +                    Box( +                        body=VSplit(buttons, padding=1, key_bindings=buttons_kb), +                        height=D(min=1, max=3, preferred=3), +                    ), +                ] +            ) +        else: +            frame_body = body + +        # Key bindings for whole dialog. +        kb = KeyBindings() +        kb.add("tab", filter=~has_completions)(focus_next) +        kb.add("s-tab", filter=~has_completions)(focus_previous) + +        frame = Shadow( +            body=Frame( +                title=lambda: self.title, +                body=frame_body, +                style="class:dialog.body", +                width=(None if with_background is None else width), +                key_bindings=kb, +                modal=modal, +            ) +        ) + +        self.container: Union[Box, Shadow] +        if with_background: +            self.container = Box(body=frame, style="class:dialog", width=width) +        else: +            self.container = frame + +    def __pt_container__(self) -> AnyContainer: +        return self.container diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/menus.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/menus.py new file mode 100644 index 00000000000..7203aae1181 --- /dev/null +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/menus.py @@ -0,0 +1,374 @@ +from typing import Callable, Iterable, List, Optional, Sequence, Union + +from prompt_toolkit.application.current import get_app +from prompt_toolkit.filters import Condition +from prompt_toolkit.formatted_text.base import OneStyleAndTextTuple, StyleAndTextTuples +from prompt_toolkit.key_binding.key_bindings import KeyBindings, KeyBindingsBase +from prompt_toolkit.key_binding.key_processor import KeyPressEvent +from prompt_toolkit.keys import Keys +from prompt_toolkit.layout.containers import ( +    AnyContainer, +    ConditionalContainer, +    Container, +    Float, +    FloatContainer, +    HSplit, +    Window, +) +from prompt_toolkit.layout.controls import FormattedTextControl +from prompt_toolkit.mouse_events import MouseEvent, MouseEventType +from prompt_toolkit.utils import get_cwidth +from prompt_toolkit.widgets import Shadow + +from .base import Border + +__all__ = [ +    "MenuContainer", +    "MenuItem", +] + +E = KeyPressEvent + + +class MenuContainer: +    """ +    :param floats: List of extra Float objects to display. +    :param menu_items: List of `MenuItem` objects. +    """ + +    def __init__( +        self, +        body: AnyContainer, +        menu_items: List["MenuItem"], +        floats: Optional[List[Float]] = None, +        key_bindings: Optional[KeyBindingsBase] = None, +    ) -> None: + +        self.body = body +        self.menu_items = menu_items +        self.selected_menu = [0] + +        # Key bindings. +        kb = KeyBindings() + +        @Condition +        def in_main_menu() -> bool: +            return len(self.selected_menu) == 1 + +        @Condition +        def in_sub_menu() -> bool: +            return len(self.selected_menu) > 1 + +        # Navigation through the main menu. + +        @kb.add("left", filter=in_main_menu) +        def _left(event: E) -> None: +            self.selected_menu[0] = max(0, self.selected_menu[0] - 1) + +        @kb.add("right", filter=in_main_menu) +        def _right(event: E) -> None: +            self.selected_menu[0] = min( +                len(self.menu_items) - 1, self.selected_menu[0] + 1 +            ) + +        @kb.add("down", filter=in_main_menu) +        def _down(event: E) -> None: +            self.selected_menu.append(0) + +        @kb.add("c-c", filter=in_main_menu) +        @kb.add("c-g", filter=in_main_menu) +        def _cancel(event: E) -> None: +            "Leave menu." +            event.app.layout.focus_last() + +        # Sub menu navigation. + +        @kb.add("left", filter=in_sub_menu) +        @kb.add("c-g", filter=in_sub_menu) +        @kb.add("c-c", filter=in_sub_menu) +        def _back(event: E) -> None: +            "Go back to parent menu." +            if len(self.selected_menu) > 1: +                self.selected_menu.pop() + +        @kb.add("right", filter=in_sub_menu) +        def _submenu(event: E) -> None: +            "go into sub menu." +            if self._get_menu(len(self.selected_menu) - 1).children: +                self.selected_menu.append(0) + +            # If This item does not have a sub menu. Go up in the parent menu. +            elif ( +                len(self.selected_menu) == 2 +                and self.selected_menu[0] < len(self.menu_items) - 1 +            ): +                self.selected_menu = [ +                    min(len(self.menu_items) - 1, self.selected_menu[0] + 1) +                ] +                if self.menu_items[self.selected_menu[0]].children: +                    self.selected_menu.append(0) + +        @kb.add("up", filter=in_sub_menu) +        def _up_in_submenu(event: E) -> None: +            "Select previous (enabled) menu item or return to main menu." +            # Look for previous enabled items in this sub menu. +            menu = self._get_menu(len(self.selected_menu) - 2) +            index = self.selected_menu[-1] + +            previous_indexes = [ +                i +                for i, item in enumerate(menu.children) +                if i < index and not item.disabled +            ] + +            if previous_indexes: +                self.selected_menu[-1] = previous_indexes[-1] +            elif len(self.selected_menu) == 2: +                # Return to main menu. +                self.selected_menu.pop() + +        @kb.add("down", filter=in_sub_menu) +        def _down_in_submenu(event: E) -> None: +            "Select next (enabled) menu item." +            menu = self._get_menu(len(self.selected_menu) - 2) +            index = self.selected_menu[-1] + +            next_indexes = [ +                i +                for i, item in enumerate(menu.children) +                if i > index and not item.disabled +            ] + +            if next_indexes: +                self.selected_menu[-1] = next_indexes[0] + +        @kb.add("enter") +        def _click(event: E) -> None: +            "Click the selected menu item." +            item = self._get_menu(len(self.selected_menu) - 1) +            if item.handler: +                event.app.layout.focus_last() +                item.handler() + +        # Controls. +        self.control = FormattedTextControl( +            self._get_menu_fragments, key_bindings=kb, focusable=True, show_cursor=False +        ) + +        self.window = Window(height=1, content=self.control, style="class:menu-bar") + +        submenu = self._submenu(0) +        submenu2 = self._submenu(1) +        submenu3 = self._submenu(2) + +        @Condition +        def has_focus() -> bool: +            return get_app().layout.current_window == self.window + +        self.container = FloatContainer( +            content=HSplit( +                [ +                    # The titlebar. +                    self.window, +                    # The 'body', like defined above. +                    body, +                ] +            ), +            floats=[ +                Float( +                    xcursor=True, +                    ycursor=True, +                    content=ConditionalContainer( +                        content=Shadow(body=submenu), filter=has_focus +                    ), +                ), +                Float( +                    attach_to_window=submenu, +                    xcursor=True, +                    ycursor=True, +                    allow_cover_cursor=True, +                    content=ConditionalContainer( +                        content=Shadow(body=submenu2), +                        filter=has_focus +                        & Condition(lambda: len(self.selected_menu) >= 1), +                    ), +                ), +                Float( +                    attach_to_window=submenu2, +                    xcursor=True, +                    ycursor=True, +                    allow_cover_cursor=True, +                    content=ConditionalContainer( +                        content=Shadow(body=submenu3), +                        filter=has_focus +                        & Condition(lambda: len(self.selected_menu) >= 2), +                    ), +                ), +                # -- +            ] +            + (floats or []), +            key_bindings=key_bindings, +        ) + +    def _get_menu(self, level: int) -> "MenuItem": +        menu = self.menu_items[self.selected_menu[0]] + +        for i, index in enumerate(self.selected_menu[1:]): +            if i < level: +                try: +                    menu = menu.children[index] +                except IndexError: +                    return MenuItem("debug") + +        return menu + +    def _get_menu_fragments(self) -> StyleAndTextTuples: +        focused = get_app().layout.has_focus(self.window) + +        # This is called during the rendering. When we discover that this +        # widget doesn't have the focus anymore. Reset menu state. +        if not focused: +            self.selected_menu = [0] + +        # Generate text fragments for the main menu. +        def one_item(i: int, item: MenuItem) -> Iterable[OneStyleAndTextTuple]: +            def mouse_handler(mouse_event: MouseEvent) -> None: +                hover = mouse_event.event_type == MouseEventType.MOUSE_MOVE +                if ( +                    mouse_event.event_type == MouseEventType.MOUSE_DOWN +                    or hover +                    and focused +                ): +                    # Toggle focus. +                    app = get_app() +                    if not hover: +                        if app.layout.has_focus(self.window): +                            if self.selected_menu == [i]: +                                app.layout.focus_last() +                        else: +                            app.layout.focus(self.window) +                    self.selected_menu = [i] + +            yield ("class:menu-bar", " ", mouse_handler) +            if i == self.selected_menu[0] and focused: +                yield ("[SetMenuPosition]", "", mouse_handler) +                style = "class:menu-bar.selected-item" +            else: +                style = "class:menu-bar" +            yield style, item.text, mouse_handler + +        result: StyleAndTextTuples = [] +        for i, item in enumerate(self.menu_items): +            result.extend(one_item(i, item)) + +        return result + +    def _submenu(self, level: int = 0) -> Window: +        def get_text_fragments() -> StyleAndTextTuples: +            result: StyleAndTextTuples = [] +            if level < len(self.selected_menu): +                menu = self._get_menu(level) +                if menu.children: +                    result.append(("class:menu", Border.TOP_LEFT)) +                    result.append(("class:menu", Border.HORIZONTAL * (menu.width + 4))) +                    result.append(("class:menu", Border.TOP_RIGHT)) +                    result.append(("", "\n")) +                    try: +                        selected_item = self.selected_menu[level + 1] +                    except IndexError: +                        selected_item = -1 + +                    def one_item( +                        i: int, item: MenuItem +                    ) -> Iterable[OneStyleAndTextTuple]: +                        def mouse_handler(mouse_event: MouseEvent) -> None: +                            if item.disabled: +                                # The arrow keys can't interact with menu items that are disabled. +                                # The mouse shouldn't be able to either. +                                return +                            hover = mouse_event.event_type == MouseEventType.MOUSE_MOVE +                            if ( +                                mouse_event.event_type == MouseEventType.MOUSE_UP +                                or hover +                            ): +                                app = get_app() +                                if not hover and item.handler: +                                    app.layout.focus_last() +                                    item.handler() +                                else: +                                    self.selected_menu = self.selected_menu[ +                                        : level + 1 +                                    ] + [i] + +                        if i == selected_item: +                            yield ("[SetCursorPosition]", "") +                            style = "class:menu-bar.selected-item" +                        else: +                            style = "" + +                        yield ("class:menu", Border.VERTICAL) +                        if item.text == "-": +                            yield ( +                                style + "class:menu-border", +                                "{}".format(Border.HORIZONTAL * (menu.width + 3)), +                                mouse_handler, +                            ) +                        else: +                            yield ( +                                style, +                                " {}".format(item.text).ljust(menu.width + 3), +                                mouse_handler, +                            ) + +                        if item.children: +                            yield (style, ">", mouse_handler) +                        else: +                            yield (style, " ", mouse_handler) + +                        if i == selected_item: +                            yield ("[SetMenuPosition]", "") +                        yield ("class:menu", Border.VERTICAL) + +                        yield ("", "\n") + +                    for i, item in enumerate(menu.children): +                        result.extend(one_item(i, item)) + +                    result.append(("class:menu", Border.BOTTOM_LEFT)) +                    result.append(("class:menu", Border.HORIZONTAL * (menu.width + 4))) +                    result.append(("class:menu", Border.BOTTOM_RIGHT)) +            return result + +        return Window(FormattedTextControl(get_text_fragments), style="class:menu") + +    @property +    def floats(self) -> Optional[List[Float]]: +        return self.container.floats + +    def __pt_container__(self) -> Container: +        return self.container + + +class MenuItem: +    def __init__( +        self, +        text: str = "", +        handler: Optional[Callable[[], None]] = None, +        children: Optional[List["MenuItem"]] = None, +        shortcut: Optional[Sequence[Union[Keys, str]]] = None, +        disabled: bool = False, +    ) -> None: + +        self.text = text +        self.handler = handler +        self.children = children or [] +        self.shortcut = shortcut +        self.disabled = disabled +        self.selected_item = 0 + +    @property +    def width(self) -> int: +        if self.children: +            return max(get_cwidth(c.text) for c in self.children) +        else: +            return 0 diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/toolbars.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/toolbars.py new file mode 100644 index 00000000000..c3559c2b83c --- /dev/null +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/toolbars.py @@ -0,0 +1,374 @@ +from typing import Any, Optional + +from prompt_toolkit.application.current import get_app +from prompt_toolkit.buffer import Buffer +from prompt_toolkit.enums import SYSTEM_BUFFER +from prompt_toolkit.filters import ( +    Condition, +    FilterOrBool, +    emacs_mode, +    has_arg, +    has_completions, +    has_focus, +    has_validation_error, +    to_filter, +    vi_mode, +    vi_navigation_mode, +) +from prompt_toolkit.formatted_text import ( +    AnyFormattedText, +    StyleAndTextTuples, +    fragment_list_len, +    to_formatted_text, +) +from prompt_toolkit.key_binding.key_bindings import ( +    ConditionalKeyBindings, +    KeyBindings, +    KeyBindingsBase, +    merge_key_bindings, +) +from prompt_toolkit.key_binding.key_processor import KeyPressEvent +from prompt_toolkit.key_binding.vi_state import InputMode +from prompt_toolkit.keys import Keys +from prompt_toolkit.layout.containers import ConditionalContainer, Container, Window +from prompt_toolkit.layout.controls import ( +    BufferControl, +    FormattedTextControl, +    SearchBufferControl, +    UIContent, +    UIControl, +) +from prompt_toolkit.layout.dimension import Dimension +from prompt_toolkit.layout.processors import BeforeInput +from prompt_toolkit.lexers import SimpleLexer +from prompt_toolkit.search import SearchDirection + +__all__ = [ +    "ArgToolbar", +    "CompletionsToolbar", +    "FormattedTextToolbar", +    "SearchToolbar", +    "SystemToolbar", +    "ValidationToolbar", +] + +E = KeyPressEvent + + +class FormattedTextToolbar(Window): +    def __init__(self, text: AnyFormattedText, style: str = "", **kw: Any) -> None: +        # Note: The style needs to be applied to the toolbar as a whole, not +        #       just the `FormattedTextControl`. +        super().__init__( +            FormattedTextControl(text, **kw), +            style=style, +            dont_extend_height=True, +            height=Dimension(min=1), +        ) + + +class SystemToolbar: +    """ +    Toolbar for a system prompt. + +    :param prompt: Prompt to be displayed to the user. +    """ + +    def __init__( +        self, +        prompt: AnyFormattedText = "Shell command: ", +        enable_global_bindings: FilterOrBool = True, +    ) -> None: + +        self.prompt = prompt +        self.enable_global_bindings = to_filter(enable_global_bindings) + +        self.system_buffer = Buffer(name=SYSTEM_BUFFER) + +        self._bindings = self._build_key_bindings() + +        self.buffer_control = BufferControl( +            buffer=self.system_buffer, +            lexer=SimpleLexer(style="class:system-toolbar.text"), +            input_processors=[ +                BeforeInput(lambda: self.prompt, style="class:system-toolbar") +            ], +            key_bindings=self._bindings, +        ) + +        self.window = Window( +            self.buffer_control, height=1, style="class:system-toolbar" +        ) + +        self.container = ConditionalContainer( +            content=self.window, filter=has_focus(self.system_buffer) +        ) + +    def _get_display_before_text(self) -> StyleAndTextTuples: +        return [ +            ("class:system-toolbar", "Shell command: "), +            ("class:system-toolbar.text", self.system_buffer.text), +            ("", "\n"), +        ] + +    def _build_key_bindings(self) -> KeyBindingsBase: +        focused = has_focus(self.system_buffer) + +        # Emacs +        emacs_bindings = KeyBindings() +        handle = emacs_bindings.add + +        @handle("escape", filter=focused) +        @handle("c-g", filter=focused) +        @handle("c-c", filter=focused) +        def _cancel(event: E) -> None: +            "Hide system prompt." +            self.system_buffer.reset() +            event.app.layout.focus_last() + +        @handle("enter", filter=focused) +        async def _accept(event: E) -> None: +            "Run system command." +            await event.app.run_system_command( +                self.system_buffer.text, +                display_before_text=self._get_display_before_text(), +            ) +            self.system_buffer.reset(append_to_history=True) +            event.app.layout.focus_last() + +        # Vi. +        vi_bindings = KeyBindings() +        handle = vi_bindings.add + +        @handle("escape", filter=focused) +        @handle("c-c", filter=focused) +        def _cancel_vi(event: E) -> None: +            "Hide system prompt." +            event.app.vi_state.input_mode = InputMode.NAVIGATION +            self.system_buffer.reset() +            event.app.layout.focus_last() + +        @handle("enter", filter=focused) +        async def _accept_vi(event: E) -> None: +            "Run system command." +            event.app.vi_state.input_mode = InputMode.NAVIGATION +            event.app.run_system_command( +                self.system_buffer.text, +                display_before_text=self._get_display_before_text(), +            ) +            self.system_buffer.reset(append_to_history=True) +            event.app.layout.focus_last() + +        # Global bindings. (Listen to these bindings, even when this widget is +        # not focussed.) +        global_bindings = KeyBindings() +        handle = global_bindings.add + +        @handle(Keys.Escape, "!", filter=~focused & emacs_mode, is_global=True) +        def _focus_me(event: E) -> None: +            "M-'!' will focus this user control." +            event.app.layout.focus(self.window) + +        @handle("!", filter=~focused & vi_mode & vi_navigation_mode, is_global=True) +        def _focus_me_vi(event: E) -> None: +            "Focus." +            event.app.vi_state.input_mode = InputMode.INSERT +            event.app.layout.focus(self.window) + +        return merge_key_bindings( +            [ +                ConditionalKeyBindings(emacs_bindings, emacs_mode), +                ConditionalKeyBindings(vi_bindings, vi_mode), +                ConditionalKeyBindings(global_bindings, self.enable_global_bindings), +            ] +        ) + +    def __pt_container__(self) -> Container: +        return self.container + + +class ArgToolbar: +    def __init__(self) -> None: +        def get_formatted_text() -> StyleAndTextTuples: +            arg = get_app().key_processor.arg or "" +            if arg == "-": +                arg = "-1" + +            return [ +                ("class:arg-toolbar", "Repeat: "), +                ("class:arg-toolbar.text", arg), +            ] + +        self.window = Window(FormattedTextControl(get_formatted_text), height=1) + +        self.container = ConditionalContainer(content=self.window, filter=has_arg) + +    def __pt_container__(self) -> Container: +        return self.container + + +class SearchToolbar: +    """ +    :param vi_mode: Display '/' and '?' instead of I-search. +    :param ignore_case: Search case insensitive. +    """ + +    def __init__( +        self, +        search_buffer: Optional[Buffer] = None, +        vi_mode: bool = False, +        text_if_not_searching: AnyFormattedText = "", +        forward_search_prompt: AnyFormattedText = "I-search: ", +        backward_search_prompt: AnyFormattedText = "I-search backward: ", +        ignore_case: FilterOrBool = False, +    ) -> None: + +        if search_buffer is None: +            search_buffer = Buffer() + +        @Condition +        def is_searching() -> bool: +            return self.control in get_app().layout.search_links + +        def get_before_input() -> AnyFormattedText: +            if not is_searching(): +                return text_if_not_searching +            elif ( +                self.control.searcher_search_state.direction == SearchDirection.BACKWARD +            ): +                return "?" if vi_mode else backward_search_prompt +            else: +                return "/" if vi_mode else forward_search_prompt + +        self.search_buffer = search_buffer + +        self.control = SearchBufferControl( +            buffer=search_buffer, +            input_processors=[ +                BeforeInput(get_before_input, style="class:search-toolbar.prompt") +            ], +            lexer=SimpleLexer(style="class:search-toolbar.text"), +            ignore_case=ignore_case, +        ) + +        self.container = ConditionalContainer( +            content=Window(self.control, height=1, style="class:search-toolbar"), +            filter=is_searching, +        ) + +    def __pt_container__(self) -> Container: +        return self.container + + +class _CompletionsToolbarControl(UIControl): +    def create_content(self, width: int, height: int) -> UIContent: +        all_fragments: StyleAndTextTuples = [] + +        complete_state = get_app().current_buffer.complete_state +        if complete_state: +            completions = complete_state.completions +            index = complete_state.complete_index  # Can be None! + +            # Width of the completions without the left/right arrows in the margins. +            content_width = width - 6 + +            # Booleans indicating whether we stripped from the left/right +            cut_left = False +            cut_right = False + +            # Create Menu content. +            fragments: StyleAndTextTuples = [] + +            for i, c in enumerate(completions): +                # When there is no more place for the next completion +                if fragment_list_len(fragments) + len(c.display_text) >= content_width: +                    # If the current one was not yet displayed, page to the next sequence. +                    if i <= (index or 0): +                        fragments = [] +                        cut_left = True +                    # If the current one is visible, stop here. +                    else: +                        cut_right = True +                        break + +                fragments.extend( +                    to_formatted_text( +                        c.display_text, +                        style=( +                            "class:completion-toolbar.completion.current" +                            if i == index +                            else "class:completion-toolbar.completion" +                        ), +                    ) +                ) +                fragments.append(("", " ")) + +            # Extend/strip until the content width. +            fragments.append(("", " " * (content_width - fragment_list_len(fragments)))) +            fragments = fragments[:content_width] + +            # Return fragments +            all_fragments.append(("", " ")) +            all_fragments.append( +                ("class:completion-toolbar.arrow", "<" if cut_left else " ") +            ) +            all_fragments.append(("", " ")) + +            all_fragments.extend(fragments) + +            all_fragments.append(("", " ")) +            all_fragments.append( +                ("class:completion-toolbar.arrow", ">" if cut_right else " ") +            ) +            all_fragments.append(("", " ")) + +        def get_line(i: int) -> StyleAndTextTuples: +            return all_fragments + +        return UIContent(get_line=get_line, line_count=1) + + +class CompletionsToolbar: +    def __init__(self) -> None: +        self.container = ConditionalContainer( +            content=Window( +                _CompletionsToolbarControl(), height=1, style="class:completion-toolbar" +            ), +            filter=has_completions, +        ) + +    def __pt_container__(self) -> Container: +        return self.container + + +class ValidationToolbar: +    def __init__(self, show_position: bool = False) -> None: +        def get_formatted_text() -> StyleAndTextTuples: +            buff = get_app().current_buffer + +            if buff.validation_error: +                row, column = buff.document.translate_index_to_position( +                    buff.validation_error.cursor_position +                ) + +                if show_position: +                    text = "%s (line=%s column=%s)" % ( +                        buff.validation_error.message, +                        row + 1, +                        column + 1, +                    ) +                else: +                    text = buff.validation_error.message + +                return [("class:validation-toolbar", text)] +            else: +                return [] + +        self.control = FormattedTextControl(get_formatted_text) + +        self.container = ConditionalContainer( +            content=Window(self.control, height=1), filter=has_validation_error +        ) + +    def __pt_container__(self) -> Container: +        return self.container | 
