You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			371 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			371 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
from __future__ import annotations
 | 
						|
 | 
						|
from typing import Any
 | 
						|
 | 
						|
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
 | 
						|
            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()
 | 
						|
 | 
						|
        # 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: Buffer | None = 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 = f"{buff.validation_error.message} (line={row + 1} column={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
 |