1from __future__ import annotations 
    2 
    3from typing import Any 
    4 
    5from prompt_toolkit.application.current import get_app 
    6from prompt_toolkit.buffer import Buffer 
    7from prompt_toolkit.enums import SYSTEM_BUFFER 
    8from prompt_toolkit.filters import ( 
    9    Condition, 
    10    FilterOrBool, 
    11    emacs_mode, 
    12    has_arg, 
    13    has_completions, 
    14    has_focus, 
    15    has_validation_error, 
    16    to_filter, 
    17    vi_mode, 
    18    vi_navigation_mode, 
    19) 
    20from prompt_toolkit.formatted_text import ( 
    21    AnyFormattedText, 
    22    StyleAndTextTuples, 
    23    fragment_list_len, 
    24    to_formatted_text, 
    25) 
    26from prompt_toolkit.key_binding.key_bindings import ( 
    27    ConditionalKeyBindings, 
    28    KeyBindings, 
    29    KeyBindingsBase, 
    30    merge_key_bindings, 
    31) 
    32from prompt_toolkit.key_binding.key_processor import KeyPressEvent 
    33from prompt_toolkit.key_binding.vi_state import InputMode 
    34from prompt_toolkit.keys import Keys 
    35from prompt_toolkit.layout.containers import ConditionalContainer, Container, Window 
    36from prompt_toolkit.layout.controls import ( 
    37    BufferControl, 
    38    FormattedTextControl, 
    39    SearchBufferControl, 
    40    UIContent, 
    41    UIControl, 
    42) 
    43from prompt_toolkit.layout.dimension import Dimension 
    44from prompt_toolkit.layout.processors import BeforeInput 
    45from prompt_toolkit.lexers import SimpleLexer 
    46from prompt_toolkit.search import SearchDirection 
    47 
    48__all__ = [ 
    49    "ArgToolbar", 
    50    "CompletionsToolbar", 
    51    "FormattedTextToolbar", 
    52    "SearchToolbar", 
    53    "SystemToolbar", 
    54    "ValidationToolbar", 
    55] 
    56 
    57E = KeyPressEvent 
    58 
    59 
    60class FormattedTextToolbar(Window): 
    61    def __init__(self, text: AnyFormattedText, style: str = "", **kw: Any) -> None: 
    62        # Note: The style needs to be applied to the toolbar as a whole, not 
    63        #       just the `FormattedTextControl`. 
    64        super().__init__( 
    65            FormattedTextControl(text, **kw), 
    66            style=style, 
    67            dont_extend_height=True, 
    68            height=Dimension(min=1), 
    69        ) 
    70 
    71 
    72class SystemToolbar: 
    73    """ 
    74    Toolbar for a system prompt. 
    75 
    76    :param prompt: Prompt to be displayed to the user. 
    77    """ 
    78 
    79    def __init__( 
    80        self, 
    81        prompt: AnyFormattedText = "Shell command: ", 
    82        enable_global_bindings: FilterOrBool = True, 
    83    ) -> None: 
    84        self.prompt = prompt 
    85        self.enable_global_bindings = to_filter(enable_global_bindings) 
    86 
    87        self.system_buffer = Buffer(name=SYSTEM_BUFFER) 
    88 
    89        self._bindings = self._build_key_bindings() 
    90 
    91        self.buffer_control = BufferControl( 
    92            buffer=self.system_buffer, 
    93            lexer=SimpleLexer(style="class:system-toolbar.text"), 
    94            input_processors=[ 
    95                BeforeInput(lambda: self.prompt, style="class:system-toolbar") 
    96            ], 
    97            key_bindings=self._bindings, 
    98        ) 
    99 
    100        self.window = Window( 
    101            self.buffer_control, height=1, style="class:system-toolbar" 
    102        ) 
    103 
    104        self.container = ConditionalContainer( 
    105            content=self.window, filter=has_focus(self.system_buffer) 
    106        ) 
    107 
    108    def _get_display_before_text(self) -> StyleAndTextTuples: 
    109        return [ 
    110            ("class:system-toolbar", "Shell command: "), 
    111            ("class:system-toolbar.text", self.system_buffer.text), 
    112            ("", "\n"), 
    113        ] 
    114 
    115    def _build_key_bindings(self) -> KeyBindingsBase: 
    116        focused = has_focus(self.system_buffer) 
    117 
    118        # Emacs 
    119        emacs_bindings = KeyBindings() 
    120        handle = emacs_bindings.add 
    121 
    122        @handle("escape", filter=focused) 
    123        @handle("c-g", filter=focused) 
    124        @handle("c-c", filter=focused) 
    125        def _cancel(event: E) -> None: 
    126            "Hide system prompt." 
    127            self.system_buffer.reset() 
    128            event.app.layout.focus_last() 
    129 
    130        @handle("enter", filter=focused) 
    131        async def _accept(event: E) -> None: 
    132            "Run system command." 
    133            await event.app.run_system_command( 
    134                self.system_buffer.text, 
    135                display_before_text=self._get_display_before_text(), 
    136            ) 
    137            self.system_buffer.reset(append_to_history=True) 
    138            event.app.layout.focus_last() 
    139 
    140        # Vi. 
    141        vi_bindings = KeyBindings() 
    142        handle = vi_bindings.add 
    143 
    144        @handle("escape", filter=focused) 
    145        @handle("c-c", filter=focused) 
    146        def _cancel_vi(event: E) -> None: 
    147            "Hide system prompt." 
    148            event.app.vi_state.input_mode = InputMode.NAVIGATION 
    149            self.system_buffer.reset() 
    150            event.app.layout.focus_last() 
    151 
    152        @handle("enter", filter=focused) 
    153        async def _accept_vi(event: E) -> None: 
    154            "Run system command." 
    155            event.app.vi_state.input_mode = InputMode.NAVIGATION 
    156            await event.app.run_system_command( 
    157                self.system_buffer.text, 
    158                display_before_text=self._get_display_before_text(), 
    159            ) 
    160            self.system_buffer.reset(append_to_history=True) 
    161            event.app.layout.focus_last() 
    162 
    163        # Global bindings. (Listen to these bindings, even when this widget is 
    164        # not focussed.) 
    165        global_bindings = KeyBindings() 
    166        handle = global_bindings.add 
    167 
    168        @handle(Keys.Escape, "!", filter=~focused & emacs_mode, is_global=True) 
    169        def _focus_me(event: E) -> None: 
    170            "M-'!' will focus this user control." 
    171            event.app.layout.focus(self.window) 
    172 
    173        @handle("!", filter=~focused & vi_mode & vi_navigation_mode, is_global=True) 
    174        def _focus_me_vi(event: E) -> None: 
    175            "Focus." 
    176            event.app.vi_state.input_mode = InputMode.INSERT 
    177            event.app.layout.focus(self.window) 
    178 
    179        return merge_key_bindings( 
    180            [ 
    181                ConditionalKeyBindings(emacs_bindings, emacs_mode), 
    182                ConditionalKeyBindings(vi_bindings, vi_mode), 
    183                ConditionalKeyBindings(global_bindings, self.enable_global_bindings), 
    184            ] 
    185        ) 
    186 
    187    def __pt_container__(self) -> Container: 
    188        return self.container 
    189 
    190 
    191class ArgToolbar: 
    192    def __init__(self) -> None: 
    193        def get_formatted_text() -> StyleAndTextTuples: 
    194            arg = get_app().key_processor.arg or "" 
    195            if arg == "-": 
    196                arg = "-1" 
    197 
    198            return [ 
    199                ("class:arg-toolbar", "Repeat: "), 
    200                ("class:arg-toolbar.text", arg), 
    201            ] 
    202 
    203        self.window = Window(FormattedTextControl(get_formatted_text), height=1) 
    204 
    205        self.container = ConditionalContainer(content=self.window, filter=has_arg) 
    206 
    207    def __pt_container__(self) -> Container: 
    208        return self.container 
    209 
    210 
    211class SearchToolbar: 
    212    """ 
    213    :param vi_mode: Display '/' and '?' instead of I-search. 
    214    :param ignore_case: Search case insensitive. 
    215    """ 
    216 
    217    def __init__( 
    218        self, 
    219        search_buffer: Buffer | None = None, 
    220        vi_mode: bool = False, 
    221        text_if_not_searching: AnyFormattedText = "", 
    222        forward_search_prompt: AnyFormattedText = "I-search: ", 
    223        backward_search_prompt: AnyFormattedText = "I-search backward: ", 
    224        ignore_case: FilterOrBool = False, 
    225    ) -> None: 
    226        if search_buffer is None: 
    227            search_buffer = Buffer() 
    228 
    229        @Condition 
    230        def is_searching() -> bool: 
    231            return self.control in get_app().layout.search_links 
    232 
    233        def get_before_input() -> AnyFormattedText: 
    234            if not is_searching(): 
    235                return text_if_not_searching 
    236            elif ( 
    237                self.control.searcher_search_state.direction == SearchDirection.BACKWARD 
    238            ): 
    239                return "?" if vi_mode else backward_search_prompt 
    240            else: 
    241                return "/" if vi_mode else forward_search_prompt 
    242 
    243        self.search_buffer = search_buffer 
    244 
    245        self.control = SearchBufferControl( 
    246            buffer=search_buffer, 
    247            input_processors=[ 
    248                BeforeInput(get_before_input, style="class:search-toolbar.prompt") 
    249            ], 
    250            lexer=SimpleLexer(style="class:search-toolbar.text"), 
    251            ignore_case=ignore_case, 
    252        ) 
    253 
    254        self.container = ConditionalContainer( 
    255            content=Window(self.control, height=1, style="class:search-toolbar"), 
    256            filter=is_searching, 
    257        ) 
    258 
    259    def __pt_container__(self) -> Container: 
    260        return self.container 
    261 
    262 
    263class _CompletionsToolbarControl(UIControl): 
    264    def create_content(self, width: int, height: int) -> UIContent: 
    265        all_fragments: StyleAndTextTuples = [] 
    266 
    267        complete_state = get_app().current_buffer.complete_state 
    268        if complete_state: 
    269            completions = complete_state.completions 
    270            index = complete_state.complete_index  # Can be None! 
    271 
    272            # Width of the completions without the left/right arrows in the margins. 
    273            content_width = width - 6 
    274 
    275            # Booleans indicating whether we stripped from the left/right 
    276            cut_left = False 
    277            cut_right = False 
    278 
    279            # Create Menu content. 
    280            fragments: StyleAndTextTuples = [] 
    281 
    282            for i, c in enumerate(completions): 
    283                # When there is no more place for the next completion 
    284                if fragment_list_len(fragments) + len(c.display_text) >= content_width: 
    285                    # If the current one was not yet displayed, page to the next sequence. 
    286                    if i <= (index or 0): 
    287                        fragments = [] 
    288                        cut_left = True 
    289                    # If the current one is visible, stop here. 
    290                    else: 
    291                        cut_right = True 
    292                        break 
    293 
    294                fragments.extend( 
    295                    to_formatted_text( 
    296                        c.display_text, 
    297                        style=( 
    298                            "class:completion-toolbar.completion.current" 
    299                            if i == index 
    300                            else "class:completion-toolbar.completion" 
    301                        ), 
    302                    ) 
    303                ) 
    304                fragments.append(("", " ")) 
    305 
    306            # Extend/strip until the content width. 
    307            fragments.append(("", " " * (content_width - fragment_list_len(fragments)))) 
    308            fragments = fragments[:content_width] 
    309 
    310            # Return fragments 
    311            all_fragments.append(("", " ")) 
    312            all_fragments.append( 
    313                ("class:completion-toolbar.arrow", "<" if cut_left else " ") 
    314            ) 
    315            all_fragments.append(("", " ")) 
    316 
    317            all_fragments.extend(fragments) 
    318 
    319            all_fragments.append(("", " ")) 
    320            all_fragments.append( 
    321                ("class:completion-toolbar.arrow", ">" if cut_right else " ") 
    322            ) 
    323            all_fragments.append(("", " ")) 
    324 
    325        def get_line(i: int) -> StyleAndTextTuples: 
    326            return all_fragments 
    327 
    328        return UIContent(get_line=get_line, line_count=1) 
    329 
    330 
    331class CompletionsToolbar: 
    332    def __init__(self) -> None: 
    333        self.container = ConditionalContainer( 
    334            content=Window( 
    335                _CompletionsToolbarControl(), height=1, style="class:completion-toolbar" 
    336            ), 
    337            filter=has_completions, 
    338        ) 
    339 
    340    def __pt_container__(self) -> Container: 
    341        return self.container 
    342 
    343 
    344class ValidationToolbar: 
    345    def __init__(self, show_position: bool = False) -> None: 
    346        def get_formatted_text() -> StyleAndTextTuples: 
    347            buff = get_app().current_buffer 
    348 
    349            if buff.validation_error: 
    350                row, column = buff.document.translate_index_to_position( 
    351                    buff.validation_error.cursor_position 
    352                ) 
    353 
    354                if show_position: 
    355                    text = f"{buff.validation_error.message} (line={row + 1} column={column + 1})" 
    356                else: 
    357                    text = buff.validation_error.message 
    358 
    359                return [("class:validation-toolbar", text)] 
    360            else: 
    361                return [] 
    362 
    363        self.control = FormattedTextControl(get_formatted_text) 
    364 
    365        self.container = ConditionalContainer( 
    366            content=Window(self.control, height=1), filter=has_validation_error 
    367        ) 
    368 
    369    def __pt_container__(self) -> Container: 
    370        return self.container