summaryrefslogtreecommitdiffstats
path: root/src/prompt_toolkit/widgets/toolbars.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/prompt_toolkit/widgets/toolbars.py')
-rw-r--r--src/prompt_toolkit/widgets/toolbars.py374
1 files changed, 374 insertions, 0 deletions
diff --git a/src/prompt_toolkit/widgets/toolbars.py b/src/prompt_toolkit/widgets/toolbars.py
new file mode 100644
index 0000000..deddf15
--- /dev/null
+++ b/src/prompt_toolkit/widgets/toolbars.py
@@ -0,0 +1,374 @@
+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 = "{} (line={} column={})".format(
+ 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