summaryrefslogtreecommitdiffstats
path: root/src/prompt_toolkit/widgets
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 16:35:31 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 16:35:31 +0000
commit4f1a3b5f9ad05aa7b08715d48909a2b06ee2fcb1 (patch)
treee5dee7be2f0d963da4faad6517278d03783e3adc /src/prompt_toolkit/widgets
parentInitial commit. (diff)
downloadprompt-toolkit-upstream/3.0.43.tar.xz
prompt-toolkit-upstream/3.0.43.zip
Adding upstream version 3.0.43.upstream/3.0.43
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--src/prompt_toolkit/widgets/__init__.py62
-rw-r--r--src/prompt_toolkit/widgets/base.py981
-rw-r--r--src/prompt_toolkit/widgets/dialogs.py107
-rw-r--r--src/prompt_toolkit/widgets/menus.py374
-rw-r--r--src/prompt_toolkit/widgets/toolbars.py374
5 files changed, 1898 insertions, 0 deletions
diff --git a/src/prompt_toolkit/widgets/__init__.py b/src/prompt_toolkit/widgets/__init__.py
new file mode 100644
index 0000000..9d1d4e3
--- /dev/null
+++ b/src/prompt_toolkit/widgets/__init__.py
@@ -0,0 +1,62 @@
+"""
+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",
+]
diff --git a/src/prompt_toolkit/widgets/base.py b/src/prompt_toolkit/widgets/base.py
new file mode 100644
index 0000000..f36a545
--- /dev/null
+++ b/src/prompt_toolkit/widgets/base.py
@@ -0,0 +1,981 @@
+"""
+Collection of reusable components for building full screen applications.
+
+All of these widgets implement the ``__pt_container__`` method, which makes
+them usable in any situation where we are expecting a `prompt_toolkit`
+container object.
+
+.. warning::
+
+ At this point, the API for these widgets is considered unstable, and can
+ potentially change between minor releases (we try not too, but no
+ guarantees are made yet). The public API in
+ `prompt_toolkit.shortcuts.dialogs` on the other hand is considered stable.
+"""
+from __future__ import annotations
+
+from functools import partial
+from typing import Callable, Generic, Sequence, TypeVar
+
+from prompt_toolkit.application.current import get_app
+from prompt_toolkit.auto_suggest import AutoSuggest, DynamicAutoSuggest
+from prompt_toolkit.buffer import Buffer, BufferAcceptHandler
+from prompt_toolkit.completion import Completer, DynamicCompleter
+from prompt_toolkit.document import Document
+from prompt_toolkit.filters import (
+ Condition,
+ FilterOrBool,
+ has_focus,
+ is_done,
+ is_true,
+ to_filter,
+)
+from prompt_toolkit.formatted_text import (
+ AnyFormattedText,
+ StyleAndTextTuples,
+ Template,
+ to_formatted_text,
+)
+from prompt_toolkit.formatted_text.utils import fragment_list_to_text
+from prompt_toolkit.history import History
+from prompt_toolkit.key_binding.key_bindings import KeyBindings
+from prompt_toolkit.key_binding.key_processor import KeyPressEvent
+from prompt_toolkit.keys import Keys
+from prompt_toolkit.layout.containers import (
+ AnyContainer,
+ ConditionalContainer,
+ Container,
+ DynamicContainer,
+ Float,
+ FloatContainer,
+ HSplit,
+ VSplit,
+ Window,
+ WindowAlign,
+)
+from prompt_toolkit.layout.controls import (
+ BufferControl,
+ FormattedTextControl,
+ GetLinePrefixCallable,
+)
+from prompt_toolkit.layout.dimension import AnyDimension, to_dimension
+from prompt_toolkit.layout.dimension import Dimension as D
+from prompt_toolkit.layout.margins import (
+ ConditionalMargin,
+ NumberedMargin,
+ ScrollbarMargin,
+)
+from prompt_toolkit.layout.processors import (
+ AppendAutoSuggestion,
+ BeforeInput,
+ ConditionalProcessor,
+ PasswordProcessor,
+ Processor,
+)
+from prompt_toolkit.lexers import DynamicLexer, Lexer
+from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
+from prompt_toolkit.utils import get_cwidth
+from prompt_toolkit.validation import DynamicValidator, Validator
+
+from .toolbars import SearchToolbar
+
+__all__ = [
+ "TextArea",
+ "Label",
+ "Button",
+ "Frame",
+ "Shadow",
+ "Box",
+ "VerticalLine",
+ "HorizontalLine",
+ "RadioList",
+ "CheckboxList",
+ "Checkbox", # backward compatibility
+ "ProgressBar",
+]
+
+E = KeyPressEvent
+
+
+class Border:
+ "Box drawing characters. (Thin)"
+
+ HORIZONTAL = "\u2500"
+ VERTICAL = "\u2502"
+ TOP_LEFT = "\u250c"
+ TOP_RIGHT = "\u2510"
+ BOTTOM_LEFT = "\u2514"
+ BOTTOM_RIGHT = "\u2518"
+
+
+class TextArea:
+ """
+ A simple input field.
+
+ This is a higher level abstraction on top of several other classes with
+ sane defaults.
+
+ This widget does have the most common options, but it does not intend to
+ cover every single use case. For more configurations options, you can
+ always build a text area manually, using a
+ :class:`~prompt_toolkit.buffer.Buffer`,
+ :class:`~prompt_toolkit.layout.BufferControl` and
+ :class:`~prompt_toolkit.layout.Window`.
+
+ Buffer attributes:
+
+ :param text: The initial text.
+ :param multiline: If True, allow multiline input.
+ :param completer: :class:`~prompt_toolkit.completion.Completer` instance
+ for auto completion.
+ :param complete_while_typing: Boolean.
+ :param accept_handler: Called when `Enter` is pressed (This should be a
+ callable that takes a buffer as input).
+ :param history: :class:`~prompt_toolkit.history.History` instance.
+ :param auto_suggest: :class:`~prompt_toolkit.auto_suggest.AutoSuggest`
+ instance for input suggestions.
+
+ BufferControl attributes:
+
+ :param password: When `True`, display using asterisks.
+ :param focusable: When `True`, allow this widget to receive the focus.
+ :param focus_on_click: When `True`, focus after mouse click.
+ :param input_processors: `None` or a list of
+ :class:`~prompt_toolkit.layout.Processor` objects.
+ :param validator: `None` or a :class:`~prompt_toolkit.validation.Validator`
+ object.
+
+ Window attributes:
+
+ :param lexer: :class:`~prompt_toolkit.lexers.Lexer` instance for syntax
+ highlighting.
+ :param wrap_lines: When `True`, don't scroll horizontally, but wrap lines.
+ :param width: Window width. (:class:`~prompt_toolkit.layout.Dimension` object.)
+ :param height: Window height. (:class:`~prompt_toolkit.layout.Dimension` object.)
+ :param scrollbar: When `True`, display a scroll bar.
+ :param style: A style string.
+ :param dont_extend_width: When `True`, don't take up more width then the
+ preferred width reported by the control.
+ :param dont_extend_height: When `True`, don't take up more width then the
+ preferred height reported by the control.
+ :param get_line_prefix: None or a callable that returns formatted text to
+ be inserted before a line. It takes a line number (int) and a
+ wrap_count and returns formatted text. This can be used for
+ implementation of line continuations, things like Vim "breakindent" and
+ so on.
+
+ Other attributes:
+
+ :param search_field: An optional `SearchToolbar` object.
+ """
+
+ def __init__(
+ self,
+ text: str = "",
+ multiline: FilterOrBool = True,
+ password: FilterOrBool = False,
+ lexer: Lexer | None = None,
+ auto_suggest: AutoSuggest | None = None,
+ completer: Completer | None = None,
+ complete_while_typing: FilterOrBool = True,
+ validator: Validator | None = None,
+ accept_handler: BufferAcceptHandler | None = None,
+ history: History | None = None,
+ focusable: FilterOrBool = True,
+ focus_on_click: FilterOrBool = False,
+ wrap_lines: FilterOrBool = True,
+ read_only: FilterOrBool = False,
+ width: AnyDimension = None,
+ height: AnyDimension = None,
+ dont_extend_height: FilterOrBool = False,
+ dont_extend_width: FilterOrBool = False,
+ line_numbers: bool = False,
+ get_line_prefix: GetLinePrefixCallable | None = None,
+ scrollbar: bool = False,
+ style: str = "",
+ search_field: SearchToolbar | None = None,
+ preview_search: FilterOrBool = True,
+ prompt: AnyFormattedText = "",
+ input_processors: list[Processor] | None = None,
+ name: str = "",
+ ) -> None:
+ if search_field is None:
+ search_control = None
+ elif isinstance(search_field, SearchToolbar):
+ search_control = search_field.control
+
+ if input_processors is None:
+ input_processors = []
+
+ # Writeable attributes.
+ self.completer = completer
+ self.complete_while_typing = complete_while_typing
+ self.lexer = lexer
+ self.auto_suggest = auto_suggest
+ self.read_only = read_only
+ self.wrap_lines = wrap_lines
+ self.validator = validator
+
+ self.buffer = Buffer(
+ document=Document(text, 0),
+ multiline=multiline,
+ read_only=Condition(lambda: is_true(self.read_only)),
+ completer=DynamicCompleter(lambda: self.completer),
+ complete_while_typing=Condition(
+ lambda: is_true(self.complete_while_typing)
+ ),
+ validator=DynamicValidator(lambda: self.validator),
+ auto_suggest=DynamicAutoSuggest(lambda: self.auto_suggest),
+ accept_handler=accept_handler,
+ history=history,
+ name=name,
+ )
+
+ self.control = BufferControl(
+ buffer=self.buffer,
+ lexer=DynamicLexer(lambda: self.lexer),
+ input_processors=[
+ ConditionalProcessor(
+ AppendAutoSuggestion(), has_focus(self.buffer) & ~is_done
+ ),
+ ConditionalProcessor(
+ processor=PasswordProcessor(), filter=to_filter(password)
+ ),
+ BeforeInput(prompt, style="class:text-area.prompt"),
+ ]
+ + input_processors,
+ search_buffer_control=search_control,
+ preview_search=preview_search,
+ focusable=focusable,
+ focus_on_click=focus_on_click,
+ )
+
+ if multiline:
+ if scrollbar:
+ right_margins = [ScrollbarMargin(display_arrows=True)]
+ else:
+ right_margins = []
+ if line_numbers:
+ left_margins = [NumberedMargin()]
+ else:
+ left_margins = []
+ else:
+ height = D.exact(1)
+ left_margins = []
+ right_margins = []
+
+ style = "class:text-area " + style
+
+ # If no height was given, guarantee height of at least 1.
+ if height is None:
+ height = D(min=1)
+
+ self.window = Window(
+ height=height,
+ width=width,
+ dont_extend_height=dont_extend_height,
+ dont_extend_width=dont_extend_width,
+ content=self.control,
+ style=style,
+ wrap_lines=Condition(lambda: is_true(self.wrap_lines)),
+ left_margins=left_margins,
+ right_margins=right_margins,
+ get_line_prefix=get_line_prefix,
+ )
+
+ @property
+ def text(self) -> str:
+ """
+ The `Buffer` text.
+ """
+ return self.buffer.text
+
+ @text.setter
+ def text(self, value: str) -> None:
+ self.document = Document(value, 0)
+
+ @property
+ def document(self) -> Document:
+ """
+ The `Buffer` document (text + cursor position).
+ """
+ return self.buffer.document
+
+ @document.setter
+ def document(self, value: Document) -> None:
+ self.buffer.set_document(value, bypass_readonly=True)
+
+ @property
+ def accept_handler(self) -> BufferAcceptHandler | None:
+ """
+ The accept handler. Called when the user accepts the input.
+ """
+ return self.buffer.accept_handler
+
+ @accept_handler.setter
+ def accept_handler(self, value: BufferAcceptHandler) -> None:
+ self.buffer.accept_handler = value
+
+ def __pt_container__(self) -> Container:
+ return self.window
+
+
+class Label:
+ """
+ Widget that displays the given text. It is not editable or focusable.
+
+ :param text: Text to display. Can be multiline. All value types accepted by
+ :class:`prompt_toolkit.layout.FormattedTextControl` are allowed,
+ including a callable.
+ :param style: A style string.
+ :param width: When given, use this width, rather than calculating it from
+ the text size.
+ :param dont_extend_width: When `True`, don't take up more width than
+ preferred, i.e. the length of the longest line of
+ the text, or value of `width` parameter, if
+ given. `True` by default
+ :param dont_extend_height: When `True`, don't take up more width than the
+ preferred height, i.e. the number of lines of
+ the text. `False` by default.
+ """
+
+ def __init__(
+ self,
+ text: AnyFormattedText,
+ style: str = "",
+ width: AnyDimension = None,
+ dont_extend_height: bool = True,
+ dont_extend_width: bool = False,
+ align: WindowAlign | Callable[[], WindowAlign] = WindowAlign.LEFT,
+ # There is no cursor navigation in a label, so it makes sense to always
+ # wrap lines by default.
+ wrap_lines: FilterOrBool = True,
+ ) -> None:
+ self.text = text
+
+ def get_width() -> AnyDimension:
+ if width is None:
+ text_fragments = to_formatted_text(self.text)
+ text = fragment_list_to_text(text_fragments)
+ if text:
+ longest_line = max(get_cwidth(line) for line in text.splitlines())
+ else:
+ return D(preferred=0)
+ return D(preferred=longest_line)
+ else:
+ return width
+
+ self.formatted_text_control = FormattedTextControl(text=lambda: self.text)
+
+ self.window = Window(
+ content=self.formatted_text_control,
+ width=get_width,
+ height=D(min=1),
+ style="class:label " + style,
+ dont_extend_height=dont_extend_height,
+ dont_extend_width=dont_extend_width,
+ align=align,
+ wrap_lines=wrap_lines,
+ )
+
+ def __pt_container__(self) -> Container:
+ return self.window
+
+
+class Button:
+ """
+ Clickable button.
+
+ :param text: The caption for the button.
+ :param handler: `None` or callable. Called when the button is clicked. No
+ parameters are passed to this callable. Use for instance Python's
+ `functools.partial` to pass parameters to this callable if needed.
+ :param width: Width of the button.
+ """
+
+ def __init__(
+ self,
+ text: str,
+ handler: Callable[[], None] | None = None,
+ width: int = 12,
+ left_symbol: str = "<",
+ right_symbol: str = ">",
+ ) -> None:
+ self.text = text
+ self.left_symbol = left_symbol
+ self.right_symbol = right_symbol
+ self.handler = handler
+ self.width = width
+ self.control = FormattedTextControl(
+ self._get_text_fragments,
+ key_bindings=self._get_key_bindings(),
+ focusable=True,
+ )
+
+ def get_style() -> str:
+ if get_app().layout.has_focus(self):
+ return "class:button.focused"
+ else:
+ return "class:button"
+
+ # Note: `dont_extend_width` is False, because we want to allow buttons
+ # to take more space if the parent container provides more space.
+ # Otherwise, we will also truncate the text.
+ # Probably we need a better way here to adjust to width of the
+ # button to the text.
+
+ self.window = Window(
+ self.control,
+ align=WindowAlign.CENTER,
+ height=1,
+ width=width,
+ style=get_style,
+ dont_extend_width=False,
+ dont_extend_height=True,
+ )
+
+ def _get_text_fragments(self) -> StyleAndTextTuples:
+ width = self.width - (
+ get_cwidth(self.left_symbol) + get_cwidth(self.right_symbol)
+ )
+ text = (f"{{:^{width}}}").format(self.text)
+
+ def handler(mouse_event: MouseEvent) -> None:
+ if (
+ self.handler is not None
+ and mouse_event.event_type == MouseEventType.MOUSE_UP
+ ):
+ self.handler()
+
+ return [
+ ("class:button.arrow", self.left_symbol, handler),
+ ("[SetCursorPosition]", ""),
+ ("class:button.text", text, handler),
+ ("class:button.arrow", self.right_symbol, handler),
+ ]
+
+ def _get_key_bindings(self) -> KeyBindings:
+ "Key bindings for the Button."
+ kb = KeyBindings()
+
+ @kb.add(" ")
+ @kb.add("enter")
+ def _(event: E) -> None:
+ if self.handler is not None:
+ self.handler()
+
+ return kb
+
+ def __pt_container__(self) -> Container:
+ return self.window
+
+
+class Frame:
+ """
+ Draw a border around any container, optionally with a title text.
+
+ Changing the title and body of the frame is possible at runtime by
+ assigning to the `body` and `title` attributes of this class.
+
+ :param body: Another container object.
+ :param title: Text to be displayed in the top of the frame (can be formatted text).
+ :param style: Style string to be applied to this widget.
+ """
+
+ def __init__(
+ self,
+ body: AnyContainer,
+ title: AnyFormattedText = "",
+ style: str = "",
+ width: AnyDimension = None,
+ height: AnyDimension = None,
+ key_bindings: KeyBindings | None = None,
+ modal: bool = False,
+ ) -> None:
+ self.title = title
+ self.body = body
+
+ fill = partial(Window, style="class:frame.border")
+ style = "class:frame " + style
+
+ top_row_with_title = VSplit(
+ [
+ fill(width=1, height=1, char=Border.TOP_LEFT),
+ fill(char=Border.HORIZONTAL),
+ fill(width=1, height=1, char="|"),
+ # Notice: we use `Template` here, because `self.title` can be an
+ # `HTML` object for instance.
+ Label(
+ lambda: Template(" {} ").format(self.title),
+ style="class:frame.label",
+ dont_extend_width=True,
+ ),
+ fill(width=1, height=1, char="|"),
+ fill(char=Border.HORIZONTAL),
+ fill(width=1, height=1, char=Border.TOP_RIGHT),
+ ],
+ height=1,
+ )
+
+ top_row_without_title = VSplit(
+ [
+ fill(width=1, height=1, char=Border.TOP_LEFT),
+ fill(char=Border.HORIZONTAL),
+ fill(width=1, height=1, char=Border.TOP_RIGHT),
+ ],
+ height=1,
+ )
+
+ @Condition
+ def has_title() -> bool:
+ return bool(self.title)
+
+ self.container = HSplit(
+ [
+ ConditionalContainer(content=top_row_with_title, filter=has_title),
+ ConditionalContainer(content=top_row_without_title, filter=~has_title),
+ VSplit(
+ [
+ fill(width=1, char=Border.VERTICAL),
+ DynamicContainer(lambda: self.body),
+ fill(width=1, char=Border.VERTICAL),
+ # Padding is required to make sure that if the content is
+ # too small, the right frame border is still aligned.
+ ],
+ padding=0,
+ ),
+ VSplit(
+ [
+ fill(width=1, height=1, char=Border.BOTTOM_LEFT),
+ fill(char=Border.HORIZONTAL),
+ fill(width=1, height=1, char=Border.BOTTOM_RIGHT),
+ ],
+ # specifying height here will increase the rendering speed.
+ height=1,
+ ),
+ ],
+ width=width,
+ height=height,
+ style=style,
+ key_bindings=key_bindings,
+ modal=modal,
+ )
+
+ def __pt_container__(self) -> Container:
+ return self.container
+
+
+class Shadow:
+ """
+ Draw a shadow underneath/behind this container.
+ (This applies `class:shadow` the the cells under the shadow. The Style
+ should define the colors for the shadow.)
+
+ :param body: Another container object.
+ """
+
+ def __init__(self, body: AnyContainer) -> None:
+ self.container = FloatContainer(
+ content=body,
+ floats=[
+ Float(
+ bottom=-1,
+ height=1,
+ left=1,
+ right=-1,
+ transparent=True,
+ content=Window(style="class:shadow"),
+ ),
+ Float(
+ bottom=-1,
+ top=1,
+ width=1,
+ right=-1,
+ transparent=True,
+ content=Window(style="class:shadow"),
+ ),
+ ],
+ )
+
+ def __pt_container__(self) -> Container:
+ return self.container
+
+
+class Box:
+ """
+ Add padding around a container.
+
+ This also makes sure that the parent can provide more space than required by
+ the child. This is very useful when wrapping a small element with a fixed
+ size into a ``VSplit`` or ``HSplit`` object. The ``HSplit`` and ``VSplit``
+ try to make sure to adapt respectively the width and height, possibly
+ shrinking other elements. Wrapping something in a ``Box`` makes it flexible.
+
+ :param body: Another container object.
+ :param padding: The margin to be used around the body. This can be
+ overridden by `padding_left`, padding_right`, `padding_top` and
+ `padding_bottom`.
+ :param style: A style string.
+ :param char: Character to be used for filling the space around the body.
+ (This is supposed to be a character with a terminal width of 1.)
+ """
+
+ def __init__(
+ self,
+ body: AnyContainer,
+ padding: AnyDimension = None,
+ padding_left: AnyDimension = None,
+ padding_right: AnyDimension = None,
+ padding_top: AnyDimension = None,
+ padding_bottom: AnyDimension = None,
+ width: AnyDimension = None,
+ height: AnyDimension = None,
+ style: str = "",
+ char: None | str | Callable[[], str] = None,
+ modal: bool = False,
+ key_bindings: KeyBindings | None = None,
+ ) -> None:
+ if padding is None:
+ padding = D(preferred=0)
+
+ def get(value: AnyDimension) -> D:
+ if value is None:
+ value = padding
+ return to_dimension(value)
+
+ self.padding_left = get(padding_left)
+ self.padding_right = get(padding_right)
+ self.padding_top = get(padding_top)
+ self.padding_bottom = get(padding_bottom)
+ self.body = body
+
+ self.container = HSplit(
+ [
+ Window(height=self.padding_top, char=char),
+ VSplit(
+ [
+ Window(width=self.padding_left, char=char),
+ body,
+ Window(width=self.padding_right, char=char),
+ ]
+ ),
+ Window(height=self.padding_bottom, char=char),
+ ],
+ width=width,
+ height=height,
+ style=style,
+ modal=modal,
+ key_bindings=None,
+ )
+
+ def __pt_container__(self) -> Container:
+ return self.container
+
+
+_T = TypeVar("_T")
+
+
+class _DialogList(Generic[_T]):
+ """
+ Common code for `RadioList` and `CheckboxList`.
+ """
+
+ open_character: str = ""
+ close_character: str = ""
+ container_style: str = ""
+ default_style: str = ""
+ selected_style: str = ""
+ checked_style: str = ""
+ multiple_selection: bool = False
+ show_scrollbar: bool = True
+
+ def __init__(
+ self,
+ values: Sequence[tuple[_T, AnyFormattedText]],
+ default_values: Sequence[_T] | None = None,
+ ) -> None:
+ assert len(values) > 0
+ default_values = default_values or []
+
+ self.values = values
+ # current_values will be used in multiple_selection,
+ # current_value will be used otherwise.
+ keys: list[_T] = [value for (value, _) in values]
+ self.current_values: list[_T] = [
+ value for value in default_values if value in keys
+ ]
+ self.current_value: _T = (
+ default_values[0]
+ if len(default_values) and default_values[0] in keys
+ else values[0][0]
+ )
+
+ # Cursor index: take first selected item or first item otherwise.
+ if len(self.current_values) > 0:
+ self._selected_index = keys.index(self.current_values[0])
+ else:
+ self._selected_index = 0
+
+ # Key bindings.
+ kb = KeyBindings()
+
+ @kb.add("up")
+ def _up(event: E) -> None:
+ self._selected_index = max(0, self._selected_index - 1)
+
+ @kb.add("down")
+ def _down(event: E) -> None:
+ self._selected_index = min(len(self.values) - 1, self._selected_index + 1)
+
+ @kb.add("pageup")
+ def _pageup(event: E) -> None:
+ w = event.app.layout.current_window
+ if w.render_info:
+ self._selected_index = max(
+ 0, self._selected_index - len(w.render_info.displayed_lines)
+ )
+
+ @kb.add("pagedown")
+ def _pagedown(event: E) -> None:
+ w = event.app.layout.current_window
+ if w.render_info:
+ self._selected_index = min(
+ len(self.values) - 1,
+ self._selected_index + len(w.render_info.displayed_lines),
+ )
+
+ @kb.add("enter")
+ @kb.add(" ")
+ def _click(event: E) -> None:
+ self._handle_enter()
+
+ @kb.add(Keys.Any)
+ def _find(event: E) -> None:
+ # We first check values after the selected value, then all values.
+ values = list(self.values)
+ for value in values[self._selected_index + 1 :] + values:
+ text = fragment_list_to_text(to_formatted_text(value[1])).lower()
+
+ if text.startswith(event.data.lower()):
+ self._selected_index = self.values.index(value)
+ return
+
+ # Control and window.
+ self.control = FormattedTextControl(
+ self._get_text_fragments, key_bindings=kb, focusable=True
+ )
+
+ self.window = Window(
+ content=self.control,
+ style=self.container_style,
+ right_margins=[
+ ConditionalMargin(
+ margin=ScrollbarMargin(display_arrows=True),
+ filter=Condition(lambda: self.show_scrollbar),
+ ),
+ ],
+ dont_extend_height=True,
+ )
+
+ def _handle_enter(self) -> None:
+ if self.multiple_selection:
+ val = self.values[self._selected_index][0]
+ if val in self.current_values:
+ self.current_values.remove(val)
+ else:
+ self.current_values.append(val)
+ else:
+ self.current_value = self.values[self._selected_index][0]
+
+ def _get_text_fragments(self) -> StyleAndTextTuples:
+ def mouse_handler(mouse_event: MouseEvent) -> None:
+ """
+ Set `_selected_index` and `current_value` according to the y
+ position of the mouse click event.
+ """
+ if mouse_event.event_type == MouseEventType.MOUSE_UP:
+ self._selected_index = mouse_event.position.y
+ self._handle_enter()
+
+ result: StyleAndTextTuples = []
+ for i, value in enumerate(self.values):
+ if self.multiple_selection:
+ checked = value[0] in self.current_values
+ else:
+ checked = value[0] == self.current_value
+ selected = i == self._selected_index
+
+ style = ""
+ if checked:
+ style += " " + self.checked_style
+ if selected:
+ style += " " + self.selected_style
+
+ result.append((style, self.open_character))
+
+ if selected:
+ result.append(("[SetCursorPosition]", ""))
+
+ if checked:
+ result.append((style, "*"))
+ else:
+ result.append((style, " "))
+
+ result.append((style, self.close_character))
+ result.append((self.default_style, " "))
+ result.extend(to_formatted_text(value[1], style=self.default_style))
+ result.append(("", "\n"))
+
+ # Add mouse handler to all fragments.
+ for i in range(len(result)):
+ result[i] = (result[i][0], result[i][1], mouse_handler)
+
+ result.pop() # Remove last newline.
+ return result
+
+ def __pt_container__(self) -> Container:
+ return self.window
+
+
+class RadioList(_DialogList[_T]):
+ """
+ List of radio buttons. Only one can be checked at the same time.
+
+ :param values: List of (value, label) tuples.
+ """
+
+ open_character = "("
+ close_character = ")"
+ container_style = "class:radio-list"
+ default_style = "class:radio"
+ selected_style = "class:radio-selected"
+ checked_style = "class:radio-checked"
+ multiple_selection = False
+
+ def __init__(
+ self,
+ values: Sequence[tuple[_T, AnyFormattedText]],
+ default: _T | None = None,
+ ) -> None:
+ if default is None:
+ default_values = None
+ else:
+ default_values = [default]
+
+ super().__init__(values, default_values=default_values)
+
+
+class CheckboxList(_DialogList[_T]):
+ """
+ List of checkbox buttons. Several can be checked at the same time.
+
+ :param values: List of (value, label) tuples.
+ """
+
+ open_character = "["
+ close_character = "]"
+ container_style = "class:checkbox-list"
+ default_style = "class:checkbox"
+ selected_style = "class:checkbox-selected"
+ checked_style = "class:checkbox-checked"
+ multiple_selection = True
+
+
+class Checkbox(CheckboxList[str]):
+ """Backward compatibility util: creates a 1-sized CheckboxList
+
+ :param text: the text
+ """
+
+ show_scrollbar = False
+
+ def __init__(self, text: AnyFormattedText = "", checked: bool = False) -> None:
+ values = [("value", text)]
+ super().__init__(values=values)
+ self.checked = checked
+
+ @property
+ def checked(self) -> bool:
+ return "value" in self.current_values
+
+ @checked.setter
+ def checked(self, value: bool) -> None:
+ if value:
+ self.current_values = ["value"]
+ else:
+ self.current_values = []
+
+
+class VerticalLine:
+ """
+ A simple vertical line with a width of 1.
+ """
+
+ def __init__(self) -> None:
+ self.window = Window(
+ char=Border.VERTICAL, style="class:line,vertical-line", width=1
+ )
+
+ def __pt_container__(self) -> Container:
+ return self.window
+
+
+class HorizontalLine:
+ """
+ A simple horizontal line with a height of 1.
+ """
+
+ def __init__(self) -> None:
+ self.window = Window(
+ char=Border.HORIZONTAL, style="class:line,horizontal-line", height=1
+ )
+
+ def __pt_container__(self) -> Container:
+ return self.window
+
+
+class ProgressBar:
+ def __init__(self) -> None:
+ self._percentage = 60
+
+ self.label = Label("60%")
+ self.container = FloatContainer(
+ content=Window(height=1),
+ floats=[
+ # We first draw the label, then the actual progress bar. Right
+ # now, this is the only way to have the colors of the progress
+ # bar appear on top of the label. The problem is that our label
+ # can't be part of any `Window` below.
+ Float(content=self.label, top=0, bottom=0),
+ Float(
+ left=0,
+ top=0,
+ right=0,
+ bottom=0,
+ content=VSplit(
+ [
+ Window(
+ style="class:progress-bar.used",
+ width=lambda: D(weight=int(self._percentage)),
+ ),
+ Window(
+ style="class:progress-bar",
+ width=lambda: D(weight=int(100 - self._percentage)),
+ ),
+ ]
+ ),
+ ),
+ ],
+ )
+
+ @property
+ def percentage(self) -> int:
+ return self._percentage
+
+ @percentage.setter
+ def percentage(self, value: int) -> None:
+ self._percentage = value
+ self.label.text = f"{value}%"
+
+ def __pt_container__(self) -> Container:
+ return self.container
diff --git a/src/prompt_toolkit/widgets/dialogs.py b/src/prompt_toolkit/widgets/dialogs.py
new file mode 100644
index 0000000..c47c15b
--- /dev/null
+++ b/src/prompt_toolkit/widgets/dialogs.py
@@ -0,0 +1,107 @@
+"""
+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
diff --git a/src/prompt_toolkit/widgets/menus.py b/src/prompt_toolkit/widgets/menus.py
new file mode 100644
index 0000000..c574c06
--- /dev/null
+++ b/src/prompt_toolkit/widgets/menus.py
@@ -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
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