|
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: |
|
|
|
|
|
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_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_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 = 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 |
|
|
|
|
|
content_width = width - 6 |
|
|
|
|
|
cut_left = False |
|
cut_right = False |
|
|
|
|
|
fragments: StyleAndTextTuples = [] |
|
|
|
for i, c in enumerate(completions): |
|
|
|
if fragment_list_len(fragments) + len(c.display_text) >= content_width: |
|
|
|
if i <= (index or 0): |
|
fragments = [] |
|
cut_left = True |
|
|
|
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(("", " ")) |
|
|
|
|
|
fragments.append(("", " " * (content_width - fragment_list_len(fragments)))) |
|
fragments = fragments[:content_width] |
|
|
|
|
|
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 |
|
|