Updates
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
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 __future__ import annotations
|
||||
|
||||
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",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
Collection of reusable components for building full screen applications.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
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: Sequence[Button] | None = 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: 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
|
||||
@@ -0,0 +1,374 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, Iterable, Sequence
|
||||
|
||||
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: list[Float] | None = None,
|
||||
key_bindings: KeyBindingsBase | None = 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",
|
||||
f"{Border.HORIZONTAL * (menu.width + 3)}",
|
||||
mouse_handler,
|
||||
)
|
||||
else:
|
||||
yield (
|
||||
style,
|
||||
f" {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) -> list[Float] | None:
|
||||
return self.container.floats
|
||||
|
||||
def __pt_container__(self) -> Container:
|
||||
return self.container
|
||||
|
||||
|
||||
class MenuItem:
|
||||
def __init__(
|
||||
self,
|
||||
text: str = "",
|
||||
handler: Callable[[], None] | None = None,
|
||||
children: list[MenuItem] | None = None,
|
||||
shortcut: Sequence[Keys | str] | None = 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
|
||||
@@ -0,0 +1,370 @@
|
||||
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
|
||||
Reference in New Issue
Block a user