diff options
author | shadchin <shadchin@yandex-team.ru> | 2022-02-10 16:44:39 +0300 |
---|---|---|
committer | Daniil Cherednik <dcherednik@yandex-team.ru> | 2022-02-10 16:44:39 +0300 |
commit | e9656aae26e0358d5378e5b63dcac5c8dbe0e4d0 (patch) | |
tree | 64175d5cadab313b3e7039ebaa06c5bc3295e274 /contrib/python/prompt-toolkit/py3/prompt_toolkit | |
parent | 2598ef1d0aee359b4b6d5fdd1758916d5907d04f (diff) | |
download | ydb-e9656aae26e0358d5378e5b63dcac5c8dbe0e4d0.tar.gz |
Restoring authorship annotation for <shadchin@yandex-team.ru>. Commit 2 of 2.
Diffstat (limited to 'contrib/python/prompt-toolkit/py3/prompt_toolkit')
141 files changed, 39519 insertions, 39519 deletions
diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/__init__.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/__init__.py index 01eac7992f..554ca2238c 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/__init__.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/__init__.py @@ -1,40 +1,40 @@ -""" -prompt_toolkit -============== - -Author: Jonathan Slenders - -Description: prompt_toolkit is a Library for building powerful interactive - command lines in Python. It can be a replacement for GNU - Readline, but it can be much more than that. - -See the examples directory to learn about the usage. - -Probably, to get started, you might also want to have a look at -`prompt_toolkit.shortcuts.prompt`. -""" -from .application import Application -from .formatted_text import ANSI, HTML -from .shortcuts import PromptSession, print_formatted_text, prompt - -# Don't forget to update in `docs/conf.py`! +""" +prompt_toolkit +============== + +Author: Jonathan Slenders + +Description: prompt_toolkit is a Library for building powerful interactive + command lines in Python. It can be a replacement for GNU + Readline, but it can be much more than that. + +See the examples directory to learn about the usage. + +Probably, to get started, you might also want to have a look at +`prompt_toolkit.shortcuts.prompt`. +""" +from .application import Application +from .formatted_text import ANSI, HTML +from .shortcuts import PromptSession, print_formatted_text, prompt + +# Don't forget to update in `docs/conf.py`! __version__ = "3.0.27" - -# Version tuple. -VERSION = tuple(__version__.split(".")) - - -__all__ = [ - # Application. - "Application", - # Shortcuts. - "prompt", - "PromptSession", - "print_formatted_text", - # Formatted text. - "HTML", - "ANSI", - # Version info. - "__version__", - "VERSION", -] + +# Version tuple. +VERSION = tuple(__version__.split(".")) + + +__all__ = [ + # Application. + "Application", + # Shortcuts. + "prompt", + "PromptSession", + "print_formatted_text", + # Formatted text. + "HTML", + "ANSI", + # Version info. + "__version__", + "VERSION", +] diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/application/__init__.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/application/__init__.py index 9bb0b04833..dc61ca73c3 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/application/__init__.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/application/__init__.py @@ -1,30 +1,30 @@ -from .application import Application -from .current import ( - AppSession, - create_app_session, +from .application import Application +from .current import ( + AppSession, + create_app_session, create_app_session_from_tty, - get_app, - get_app_or_none, - get_app_session, - set_app, -) -from .dummy import DummyApplication -from .run_in_terminal import in_terminal, run_in_terminal - -__all__ = [ - # Application. - "Application", - # Current. - "AppSession", - "get_app_session", - "create_app_session", + get_app, + get_app_or_none, + get_app_session, + set_app, +) +from .dummy import DummyApplication +from .run_in_terminal import in_terminal, run_in_terminal + +__all__ = [ + # Application. + "Application", + # Current. + "AppSession", + "get_app_session", + "create_app_session", "create_app_session_from_tty", - "get_app", - "get_app_or_none", - "set_app", - # Dummy. - "DummyApplication", - # Run_in_terminal - "in_terminal", - "run_in_terminal", -] + "get_app", + "get_app_or_none", + "set_app", + # Dummy. + "DummyApplication", + # Run_in_terminal + "in_terminal", + "run_in_terminal", +] diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/application/application.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/application/application.py index 40b9d66a22..5426ebfdfe 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/application/application.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/application/application.py @@ -1,658 +1,658 @@ -import asyncio -import os -import re -import signal -import sys -import threading -import time -from asyncio import ( - AbstractEventLoop, - CancelledError, - Future, - Task, - ensure_future, - new_event_loop, - set_event_loop, - sleep, -) -from contextlib import contextmanager -from subprocess import Popen -from traceback import format_tb -from typing import ( - Any, - Awaitable, - Callable, - Dict, - FrozenSet, - Generator, - Generic, - Hashable, - Iterable, - List, - Optional, - Tuple, - Type, - TypeVar, - Union, - cast, - overload, -) - -from prompt_toolkit.buffer import Buffer -from prompt_toolkit.cache import SimpleCache -from prompt_toolkit.clipboard import Clipboard, InMemoryClipboard +import asyncio +import os +import re +import signal +import sys +import threading +import time +from asyncio import ( + AbstractEventLoop, + CancelledError, + Future, + Task, + ensure_future, + new_event_loop, + set_event_loop, + sleep, +) +from contextlib import contextmanager +from subprocess import Popen +from traceback import format_tb +from typing import ( + Any, + Awaitable, + Callable, + Dict, + FrozenSet, + Generator, + Generic, + Hashable, + Iterable, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, + cast, + overload, +) + +from prompt_toolkit.buffer import Buffer +from prompt_toolkit.cache import SimpleCache +from prompt_toolkit.clipboard import Clipboard, InMemoryClipboard from prompt_toolkit.cursor_shapes import AnyCursorShapeConfig, to_cursor_shape_config -from prompt_toolkit.data_structures import Size -from prompt_toolkit.enums import EditingMode -from prompt_toolkit.eventloop import ( - get_traceback_from_context, - run_in_executor_with_context, -) -from prompt_toolkit.eventloop.utils import call_soon_threadsafe, get_event_loop -from prompt_toolkit.filters import Condition, Filter, FilterOrBool, to_filter -from prompt_toolkit.formatted_text import AnyFormattedText -from prompt_toolkit.input.base import Input -from prompt_toolkit.input.typeahead import get_typeahead, store_typeahead -from prompt_toolkit.key_binding.bindings.page_navigation import ( - load_page_navigation_bindings, -) -from prompt_toolkit.key_binding.defaults import load_key_bindings -from prompt_toolkit.key_binding.emacs_state import EmacsState -from prompt_toolkit.key_binding.key_bindings import ( - Binding, - ConditionalKeyBindings, - GlobalOnlyKeyBindings, - KeyBindings, - KeyBindingsBase, - KeysTuple, - merge_key_bindings, -) -from prompt_toolkit.key_binding.key_processor import KeyPressEvent, KeyProcessor -from prompt_toolkit.key_binding.vi_state import ViState -from prompt_toolkit.keys import Keys -from prompt_toolkit.layout.containers import Container, Window -from prompt_toolkit.layout.controls import BufferControl, UIControl -from prompt_toolkit.layout.dummy import create_dummy_layout -from prompt_toolkit.layout.layout import Layout, walk -from prompt_toolkit.output import ColorDepth, Output -from prompt_toolkit.renderer import Renderer, print_formatted_text -from prompt_toolkit.search import SearchState -from prompt_toolkit.styles import ( - BaseStyle, - DummyStyle, - DummyStyleTransformation, - DynamicStyle, - StyleTransformation, - default_pygments_style, - default_ui_style, - merge_styles, -) -from prompt_toolkit.utils import Event, in_main_thread - +from prompt_toolkit.data_structures import Size +from prompt_toolkit.enums import EditingMode +from prompt_toolkit.eventloop import ( + get_traceback_from_context, + run_in_executor_with_context, +) +from prompt_toolkit.eventloop.utils import call_soon_threadsafe, get_event_loop +from prompt_toolkit.filters import Condition, Filter, FilterOrBool, to_filter +from prompt_toolkit.formatted_text import AnyFormattedText +from prompt_toolkit.input.base import Input +from prompt_toolkit.input.typeahead import get_typeahead, store_typeahead +from prompt_toolkit.key_binding.bindings.page_navigation import ( + load_page_navigation_bindings, +) +from prompt_toolkit.key_binding.defaults import load_key_bindings +from prompt_toolkit.key_binding.emacs_state import EmacsState +from prompt_toolkit.key_binding.key_bindings import ( + Binding, + ConditionalKeyBindings, + GlobalOnlyKeyBindings, + KeyBindings, + KeyBindingsBase, + KeysTuple, + merge_key_bindings, +) +from prompt_toolkit.key_binding.key_processor import KeyPressEvent, KeyProcessor +from prompt_toolkit.key_binding.vi_state import ViState +from prompt_toolkit.keys import Keys +from prompt_toolkit.layout.containers import Container, Window +from prompt_toolkit.layout.controls import BufferControl, UIControl +from prompt_toolkit.layout.dummy import create_dummy_layout +from prompt_toolkit.layout.layout import Layout, walk +from prompt_toolkit.output import ColorDepth, Output +from prompt_toolkit.renderer import Renderer, print_formatted_text +from prompt_toolkit.search import SearchState +from prompt_toolkit.styles import ( + BaseStyle, + DummyStyle, + DummyStyleTransformation, + DynamicStyle, + StyleTransformation, + default_pygments_style, + default_ui_style, + merge_styles, +) +from prompt_toolkit.utils import Event, in_main_thread + from ..utils import is_windows -from .current import get_app_session, set_app -from .run_in_terminal import in_terminal, run_in_terminal - -try: - import contextvars -except ImportError: - import prompt_toolkit.eventloop.dummy_contextvars as contextvars # type: ignore - - -__all__ = [ - "Application", -] - - -E = KeyPressEvent -_AppResult = TypeVar("_AppResult") -ApplicationEventHandler = Callable[["Application[_AppResult]"], None] - -_SIGWINCH = getattr(signal, "SIGWINCH", None) -_SIGTSTP = getattr(signal, "SIGTSTP", None) - - -class Application(Generic[_AppResult]): - """ - The main Application class! - This glues everything together. - - :param layout: A :class:`~prompt_toolkit.layout.Layout` instance. - :param key_bindings: - :class:`~prompt_toolkit.key_binding.KeyBindingsBase` instance for - the key bindings. - :param clipboard: :class:`~prompt_toolkit.clipboard.Clipboard` to use. - :param full_screen: When True, run the application on the alternate screen buffer. - :param color_depth: Any :class:`~.ColorDepth` value, a callable that - returns a :class:`~.ColorDepth` or `None` for default. - :param erase_when_done: (bool) Clear the application output when it finishes. - :param reverse_vi_search_direction: Normally, in Vi mode, a '/' searches - forward and a '?' searches backward. In Readline mode, this is usually - reversed. - :param min_redraw_interval: Number of seconds to wait between redraws. Use - this for applications where `invalidate` is called a lot. This could cause - a lot of terminal output, which some terminals are not able to process. - - `None` means that every `invalidate` will be scheduled right away - (which is usually fine). - - When one `invalidate` is called, but a scheduled redraw of a previous - `invalidate` call has not been executed yet, nothing will happen in any - case. - - :param max_render_postpone_time: When there is high CPU (a lot of other - scheduled calls), postpone the rendering max x seconds. '0' means: - don't postpone. '.5' means: try to draw at least twice a second. - - :param refresh_interval: Automatically invalidate the UI every so many - seconds. When `None` (the default), only invalidate when `invalidate` - has been called. - - :param terminal_size_polling_interval: Poll the terminal size every so many - seconds. Useful if the applications runs in a thread other then then - main thread where SIGWINCH can't be handled, or on Windows. - - Filters: - - :param mouse_support: (:class:`~prompt_toolkit.filters.Filter` or - boolean). When True, enable mouse support. - :param paste_mode: :class:`~prompt_toolkit.filters.Filter` or boolean. - :param editing_mode: :class:`~prompt_toolkit.enums.EditingMode`. - - :param enable_page_navigation_bindings: When `True`, enable the page - navigation key bindings. These include both Emacs and Vi bindings like - page-up, page-down and so on to scroll through pages. Mostly useful for - creating an editor or other full screen applications. Probably, you - don't want this for the implementation of a REPL. By default, this is - enabled if `full_screen` is set. - - Callbacks (all of these should accept an - :class:`~prompt_toolkit.application.Application` object as input.) - - :param on_reset: Called during reset. - :param on_invalidate: Called when the UI has been invalidated. - :param before_render: Called right before rendering. - :param after_render: Called right after rendering. - - I/O: - (Note that the preferred way to change the input/output is by creating an - `AppSession` with the required input/output objects. If you need multiple - applications running at the same time, you have to create a separate - `AppSession` using a `with create_app_session():` block. - - :param input: :class:`~prompt_toolkit.input.Input` instance. - :param output: :class:`~prompt_toolkit.output.Output` instance. (Probably - Vt100_Output or Win32Output.) - - Usage: - - app = Application(...) - app.run() - - # Or - await app.run_async() - """ - - def __init__( - self, - layout: Optional[Layout] = None, - style: Optional[BaseStyle] = None, - include_default_pygments_style: FilterOrBool = True, - style_transformation: Optional[StyleTransformation] = None, - key_bindings: Optional[KeyBindingsBase] = None, - clipboard: Optional[Clipboard] = None, - full_screen: bool = False, - color_depth: Union[ - ColorDepth, Callable[[], Union[ColorDepth, None]], None - ] = None, - mouse_support: FilterOrBool = False, - enable_page_navigation_bindings: Optional[ - FilterOrBool - ] = None, # Can be None, True or False. - paste_mode: FilterOrBool = False, - editing_mode: EditingMode = EditingMode.EMACS, - erase_when_done: bool = False, - reverse_vi_search_direction: FilterOrBool = False, - min_redraw_interval: Union[float, int, None] = None, - max_render_postpone_time: Union[float, int, None] = 0.01, - refresh_interval: Optional[float] = None, - terminal_size_polling_interval: Optional[float] = 0.5, +from .current import get_app_session, set_app +from .run_in_terminal import in_terminal, run_in_terminal + +try: + import contextvars +except ImportError: + import prompt_toolkit.eventloop.dummy_contextvars as contextvars # type: ignore + + +__all__ = [ + "Application", +] + + +E = KeyPressEvent +_AppResult = TypeVar("_AppResult") +ApplicationEventHandler = Callable[["Application[_AppResult]"], None] + +_SIGWINCH = getattr(signal, "SIGWINCH", None) +_SIGTSTP = getattr(signal, "SIGTSTP", None) + + +class Application(Generic[_AppResult]): + """ + The main Application class! + This glues everything together. + + :param layout: A :class:`~prompt_toolkit.layout.Layout` instance. + :param key_bindings: + :class:`~prompt_toolkit.key_binding.KeyBindingsBase` instance for + the key bindings. + :param clipboard: :class:`~prompt_toolkit.clipboard.Clipboard` to use. + :param full_screen: When True, run the application on the alternate screen buffer. + :param color_depth: Any :class:`~.ColorDepth` value, a callable that + returns a :class:`~.ColorDepth` or `None` for default. + :param erase_when_done: (bool) Clear the application output when it finishes. + :param reverse_vi_search_direction: Normally, in Vi mode, a '/' searches + forward and a '?' searches backward. In Readline mode, this is usually + reversed. + :param min_redraw_interval: Number of seconds to wait between redraws. Use + this for applications where `invalidate` is called a lot. This could cause + a lot of terminal output, which some terminals are not able to process. + + `None` means that every `invalidate` will be scheduled right away + (which is usually fine). + + When one `invalidate` is called, but a scheduled redraw of a previous + `invalidate` call has not been executed yet, nothing will happen in any + case. + + :param max_render_postpone_time: When there is high CPU (a lot of other + scheduled calls), postpone the rendering max x seconds. '0' means: + don't postpone. '.5' means: try to draw at least twice a second. + + :param refresh_interval: Automatically invalidate the UI every so many + seconds. When `None` (the default), only invalidate when `invalidate` + has been called. + + :param terminal_size_polling_interval: Poll the terminal size every so many + seconds. Useful if the applications runs in a thread other then then + main thread where SIGWINCH can't be handled, or on Windows. + + Filters: + + :param mouse_support: (:class:`~prompt_toolkit.filters.Filter` or + boolean). When True, enable mouse support. + :param paste_mode: :class:`~prompt_toolkit.filters.Filter` or boolean. + :param editing_mode: :class:`~prompt_toolkit.enums.EditingMode`. + + :param enable_page_navigation_bindings: When `True`, enable the page + navigation key bindings. These include both Emacs and Vi bindings like + page-up, page-down and so on to scroll through pages. Mostly useful for + creating an editor or other full screen applications. Probably, you + don't want this for the implementation of a REPL. By default, this is + enabled if `full_screen` is set. + + Callbacks (all of these should accept an + :class:`~prompt_toolkit.application.Application` object as input.) + + :param on_reset: Called during reset. + :param on_invalidate: Called when the UI has been invalidated. + :param before_render: Called right before rendering. + :param after_render: Called right after rendering. + + I/O: + (Note that the preferred way to change the input/output is by creating an + `AppSession` with the required input/output objects. If you need multiple + applications running at the same time, you have to create a separate + `AppSession` using a `with create_app_session():` block. + + :param input: :class:`~prompt_toolkit.input.Input` instance. + :param output: :class:`~prompt_toolkit.output.Output` instance. (Probably + Vt100_Output or Win32Output.) + + Usage: + + app = Application(...) + app.run() + + # Or + await app.run_async() + """ + + def __init__( + self, + layout: Optional[Layout] = None, + style: Optional[BaseStyle] = None, + include_default_pygments_style: FilterOrBool = True, + style_transformation: Optional[StyleTransformation] = None, + key_bindings: Optional[KeyBindingsBase] = None, + clipboard: Optional[Clipboard] = None, + full_screen: bool = False, + color_depth: Union[ + ColorDepth, Callable[[], Union[ColorDepth, None]], None + ] = None, + mouse_support: FilterOrBool = False, + enable_page_navigation_bindings: Optional[ + FilterOrBool + ] = None, # Can be None, True or False. + paste_mode: FilterOrBool = False, + editing_mode: EditingMode = EditingMode.EMACS, + erase_when_done: bool = False, + reverse_vi_search_direction: FilterOrBool = False, + min_redraw_interval: Union[float, int, None] = None, + max_render_postpone_time: Union[float, int, None] = 0.01, + refresh_interval: Optional[float] = None, + terminal_size_polling_interval: Optional[float] = 0.5, cursor: AnyCursorShapeConfig = None, - on_reset: Optional["ApplicationEventHandler[_AppResult]"] = None, - on_invalidate: Optional["ApplicationEventHandler[_AppResult]"] = None, - before_render: Optional["ApplicationEventHandler[_AppResult]"] = None, - after_render: Optional["ApplicationEventHandler[_AppResult]"] = None, - # I/O. - input: Optional[Input] = None, - output: Optional[Output] = None, - ) -> None: - - # If `enable_page_navigation_bindings` is not specified, enable it in - # case of full screen applications only. This can be overridden by the user. - if enable_page_navigation_bindings is None: - enable_page_navigation_bindings = Condition(lambda: self.full_screen) - - paste_mode = to_filter(paste_mode) - mouse_support = to_filter(mouse_support) - reverse_vi_search_direction = to_filter(reverse_vi_search_direction) - enable_page_navigation_bindings = to_filter(enable_page_navigation_bindings) - include_default_pygments_style = to_filter(include_default_pygments_style) - - if layout is None: - layout = create_dummy_layout() - - if style_transformation is None: - style_transformation = DummyStyleTransformation() - - self.style = style - self.style_transformation = style_transformation - - # Key bindings. - self.key_bindings = key_bindings - self._default_bindings = load_key_bindings() - self._page_navigation_bindings = load_page_navigation_bindings() - - self.layout = layout - self.clipboard = clipboard or InMemoryClipboard() - self.full_screen: bool = full_screen - self._color_depth = color_depth - self.mouse_support = mouse_support - - self.paste_mode = paste_mode - self.editing_mode = editing_mode - self.erase_when_done = erase_when_done - self.reverse_vi_search_direction = reverse_vi_search_direction - self.enable_page_navigation_bindings = enable_page_navigation_bindings - self.min_redraw_interval = min_redraw_interval - self.max_render_postpone_time = max_render_postpone_time - self.refresh_interval = refresh_interval - self.terminal_size_polling_interval = terminal_size_polling_interval - + on_reset: Optional["ApplicationEventHandler[_AppResult]"] = None, + on_invalidate: Optional["ApplicationEventHandler[_AppResult]"] = None, + before_render: Optional["ApplicationEventHandler[_AppResult]"] = None, + after_render: Optional["ApplicationEventHandler[_AppResult]"] = None, + # I/O. + input: Optional[Input] = None, + output: Optional[Output] = None, + ) -> None: + + # If `enable_page_navigation_bindings` is not specified, enable it in + # case of full screen applications only. This can be overridden by the user. + if enable_page_navigation_bindings is None: + enable_page_navigation_bindings = Condition(lambda: self.full_screen) + + paste_mode = to_filter(paste_mode) + mouse_support = to_filter(mouse_support) + reverse_vi_search_direction = to_filter(reverse_vi_search_direction) + enable_page_navigation_bindings = to_filter(enable_page_navigation_bindings) + include_default_pygments_style = to_filter(include_default_pygments_style) + + if layout is None: + layout = create_dummy_layout() + + if style_transformation is None: + style_transformation = DummyStyleTransformation() + + self.style = style + self.style_transformation = style_transformation + + # Key bindings. + self.key_bindings = key_bindings + self._default_bindings = load_key_bindings() + self._page_navigation_bindings = load_page_navigation_bindings() + + self.layout = layout + self.clipboard = clipboard or InMemoryClipboard() + self.full_screen: bool = full_screen + self._color_depth = color_depth + self.mouse_support = mouse_support + + self.paste_mode = paste_mode + self.editing_mode = editing_mode + self.erase_when_done = erase_when_done + self.reverse_vi_search_direction = reverse_vi_search_direction + self.enable_page_navigation_bindings = enable_page_navigation_bindings + self.min_redraw_interval = min_redraw_interval + self.max_render_postpone_time = max_render_postpone_time + self.refresh_interval = refresh_interval + self.terminal_size_polling_interval = terminal_size_polling_interval + self.cursor = to_cursor_shape_config(cursor) - # Events. - self.on_invalidate = Event(self, on_invalidate) - self.on_reset = Event(self, on_reset) - self.before_render = Event(self, before_render) - self.after_render = Event(self, after_render) - - # I/O. - session = get_app_session() - self.output = output or session.output - self.input = input or session.input - - # List of 'extra' functions to execute before a Application.run. - self.pre_run_callables: List[Callable[[], None]] = [] - - self._is_running = False - self.future: Optional[Future[_AppResult]] = None - self.loop: Optional[AbstractEventLoop] = None - self.context: Optional[contextvars.Context] = None - - #: Quoted insert. This flag is set if we go into quoted insert mode. - self.quoted_insert = False - - #: Vi state. (For Vi key bindings.) - self.vi_state = ViState() - self.emacs_state = EmacsState() - - #: When to flush the input (For flushing escape keys.) This is important - #: on terminals that use vt100 input. We can't distinguish the escape - #: key from for instance the left-arrow key, if we don't know what follows - #: after "\x1b". This little timer will consider "\x1b" to be escape if - #: nothing did follow in this time span. - #: This seems to work like the `ttimeoutlen` option in Vim. - self.ttimeoutlen = 0.5 # Seconds. - - #: Like Vim's `timeoutlen` option. This can be `None` or a float. For - #: instance, suppose that we have a key binding AB and a second key - #: binding A. If the uses presses A and then waits, we don't handle - #: this binding yet (unless it was marked 'eager'), because we don't - #: know what will follow. This timeout is the maximum amount of time - #: that we wait until we call the handlers anyway. Pass `None` to - #: disable this timeout. - self.timeoutlen = 1.0 - - #: The `Renderer` instance. - # Make sure that the same stdout is used, when a custom renderer has been passed. - self._merged_style = self._create_merged_style(include_default_pygments_style) - - self.renderer = Renderer( - self._merged_style, - self.output, - full_screen=full_screen, - mouse_support=mouse_support, - cpr_not_supported_callback=self.cpr_not_supported_callback, - ) - - #: Render counter. This one is increased every time the UI is rendered. - #: It can be used as a key for caching certain information during one - #: rendering. - self.render_counter = 0 - - # Invalidate flag. When 'True', a repaint has been scheduled. - self._invalidated = False - self._invalidate_events: List[ - Event[object] - ] = [] # Collection of 'invalidate' Event objects. - self._last_redraw_time = 0.0 # Unix timestamp of last redraw. Used when - # `min_redraw_interval` is given. - - #: The `InputProcessor` instance. - self.key_processor = KeyProcessor(_CombinedRegistry(self)) - - # If `run_in_terminal` was called. This will point to a `Future` what will be - # set at the point when the previous run finishes. - self._running_in_terminal = False - self._running_in_terminal_f: Optional[Future[None]] = None - - # Trigger initialize callback. - self.reset() - - def _create_merged_style(self, include_default_pygments_style: Filter) -> BaseStyle: - """ - Create a `Style` object that merges the default UI style, the default - pygments style, and the custom user style. - """ - dummy_style = DummyStyle() - pygments_style = default_pygments_style() - - @DynamicStyle - def conditional_pygments_style() -> BaseStyle: - if include_default_pygments_style(): - return pygments_style - else: - return dummy_style - - return merge_styles( - [ - default_ui_style(), - conditional_pygments_style, - DynamicStyle(lambda: self.style), - ] - ) - - @property - def color_depth(self) -> ColorDepth: - """ - The active :class:`.ColorDepth`. - - The current value is determined as follows: - - - If a color depth was given explicitly to this application, use that - value. - - Otherwise, fall back to the color depth that is reported by the - :class:`.Output` implementation. If the :class:`.Output` class was - created using `output.defaults.create_output`, then this value is - coming from the $PROMPT_TOOLKIT_COLOR_DEPTH environment variable. - """ - depth = self._color_depth - - if callable(depth): - depth = depth() - - if depth is None: - depth = self.output.get_default_color_depth() - - return depth - - @property - def current_buffer(self) -> Buffer: - """ - The currently focused :class:`~.Buffer`. - - (This returns a dummy :class:`.Buffer` when none of the actual buffers - has the focus. In this case, it's really not practical to check for - `None` values or catch exceptions every time.) - """ - return self.layout.current_buffer or Buffer( - name="dummy-buffer" - ) # Dummy buffer. - - @property - def current_search_state(self) -> SearchState: - """ - Return the current :class:`.SearchState`. (The one for the focused - :class:`.BufferControl`.) - """ - ui_control = self.layout.current_control - if isinstance(ui_control, BufferControl): - return ui_control.search_state - else: - return SearchState() # Dummy search state. (Don't return None!) - - def reset(self) -> None: - """ - Reset everything, for reading the next input. - """ - # Notice that we don't reset the buffers. (This happens just before - # returning, and when we have multiple buffers, we clearly want the - # content in the other buffers to remain unchanged between several - # calls of `run`. (And the same is true for the focus stack.) - - self.exit_style = "" - - self.background_tasks: List[Task[None]] = [] - - self.renderer.reset() - self.key_processor.reset() - self.layout.reset() - self.vi_state.reset() - self.emacs_state.reset() - - # Trigger reset event. - self.on_reset.fire() - - # Make sure that we have a 'focusable' widget focused. - # (The `Layout` class can't determine this.) - layout = self.layout - - if not layout.current_control.is_focusable(): - for w in layout.find_all_windows(): - if w.content.is_focusable(): - layout.current_window = w - break - - def invalidate(self) -> None: - """ - Thread safe way of sending a repaint trigger to the input event loop. - """ - if not self._is_running: - # Don't schedule a redraw if we're not running. - # Otherwise, `get_event_loop()` in `call_soon_threadsafe` can fail. - # See: https://github.com/dbcli/mycli/issues/797 - return - - # `invalidate()` called if we don't have a loop yet (not running?), or - # after the event loop was closed. - if self.loop is None or self.loop.is_closed(): - return - - # Never schedule a second redraw, when a previous one has not yet been - # executed. (This should protect against other threads calling - # 'invalidate' many times, resulting in 100% CPU.) - if self._invalidated: - return - else: - self._invalidated = True - - # Trigger event. - self.loop.call_soon_threadsafe(self.on_invalidate.fire) - - def redraw() -> None: - self._invalidated = False - self._redraw() - - def schedule_redraw() -> None: - call_soon_threadsafe( - redraw, max_postpone_time=self.max_render_postpone_time, loop=self.loop - ) - - if self.min_redraw_interval: - # When a minimum redraw interval is set, wait minimum this amount - # of time between redraws. - diff = time.time() - self._last_redraw_time - if diff < self.min_redraw_interval: - - async def redraw_in_future() -> None: - await sleep(cast(float, self.min_redraw_interval) - diff) - schedule_redraw() - - self.loop.call_soon_threadsafe( - lambda: self.create_background_task(redraw_in_future()) - ) - else: - schedule_redraw() - else: - schedule_redraw() - - @property - def invalidated(self) -> bool: - "True when a redraw operation has been scheduled." - return self._invalidated - - def _redraw(self, render_as_done: bool = False) -> None: - """ - Render the command line again. (Not thread safe!) (From other threads, - or if unsure, use :meth:`.Application.invalidate`.) - - :param render_as_done: make sure to put the cursor after the UI. - """ - - def run_in_context() -> None: - # Only draw when no sub application was started. - if self._is_running and not self._running_in_terminal: - if self.min_redraw_interval: - self._last_redraw_time = time.time() - - # Render - self.render_counter += 1 - self.before_render.fire() - - if render_as_done: - if self.erase_when_done: - self.renderer.erase() - else: - # Draw in 'done' state and reset renderer. - self.renderer.render(self, self.layout, is_done=render_as_done) - else: - self.renderer.render(self, self.layout) - - self.layout.update_parents_relations() - - # Fire render event. - self.after_render.fire() - - self._update_invalidate_events() - - # NOTE: We want to make sure this Application is the active one. The - # invalidate function is often called from a context where this - # application is not the active one. (Like the - # `PromptSession._auto_refresh_context`). - # We copy the context in case the context was already active, to - # prevent RuntimeErrors. (The rendering is not supposed to change - # any context variables.) - if self.context is not None: - self.context.copy().run(run_in_context) - - def _start_auto_refresh_task(self) -> None: - """ - Start a while/true loop in the background for automatic invalidation of - the UI. - """ - if self.refresh_interval is not None and self.refresh_interval != 0: - - async def auto_refresh(refresh_interval: float) -> None: - while True: - await sleep(refresh_interval) - self.invalidate() - - self.create_background_task(auto_refresh(self.refresh_interval)) - - def _update_invalidate_events(self) -> None: - """ - Make sure to attach 'invalidate' handlers to all invalidate events in - the UI. - """ - # Remove all the original event handlers. (Components can be removed - # from the UI.) - for ev in self._invalidate_events: - ev -= self._invalidate_handler - - # Gather all new events. - # (All controls are able to invalidate themselves.) - def gather_events() -> Iterable[Event[object]]: - for c in self.layout.find_all_controls(): - for ev in c.get_invalidate_events(): - yield ev - - self._invalidate_events = list(gather_events()) - - for ev in self._invalidate_events: - ev += self._invalidate_handler - - def _invalidate_handler(self, sender: object) -> None: - """ - Handler for invalidate events coming from UIControls. - - (This handles the difference in signature between event handler and - `self.invalidate`. It also needs to be a method -not a nested - function-, so that we can remove it again .) - """ - self.invalidate() - - def _on_resize(self) -> None: - """ - When the window size changes, we erase the current output and request - again the cursor position. When the CPR answer arrives, the output is - drawn again. - """ - # Erase, request position (when cursor is at the start position) - # and redraw again. -- The order is important. - self.renderer.erase(leave_alternate_screen=False) - self._request_absolute_cursor_position() - self._redraw() - - def _pre_run(self, pre_run: Optional[Callable[[], None]] = None) -> None: - """ - Called during `run`. - - `self.future` should be set to the new future at the point where this - is called in order to avoid data races. `pre_run` can be used to set a - `threading.Event` to synchronize with UI termination code, running in - another thread that would call `Application.exit`. (See the progress - bar code for an example.) - """ - if pre_run: - pre_run() - - # Process registered "pre_run_callables" and clear list. - for c in self.pre_run_callables: - c() - del self.pre_run_callables[:] - - async def run_async( - self, - pre_run: Optional[Callable[[], None]] = None, - set_exception_handler: bool = True, + # Events. + self.on_invalidate = Event(self, on_invalidate) + self.on_reset = Event(self, on_reset) + self.before_render = Event(self, before_render) + self.after_render = Event(self, after_render) + + # I/O. + session = get_app_session() + self.output = output or session.output + self.input = input or session.input + + # List of 'extra' functions to execute before a Application.run. + self.pre_run_callables: List[Callable[[], None]] = [] + + self._is_running = False + self.future: Optional[Future[_AppResult]] = None + self.loop: Optional[AbstractEventLoop] = None + self.context: Optional[contextvars.Context] = None + + #: Quoted insert. This flag is set if we go into quoted insert mode. + self.quoted_insert = False + + #: Vi state. (For Vi key bindings.) + self.vi_state = ViState() + self.emacs_state = EmacsState() + + #: When to flush the input (For flushing escape keys.) This is important + #: on terminals that use vt100 input. We can't distinguish the escape + #: key from for instance the left-arrow key, if we don't know what follows + #: after "\x1b". This little timer will consider "\x1b" to be escape if + #: nothing did follow in this time span. + #: This seems to work like the `ttimeoutlen` option in Vim. + self.ttimeoutlen = 0.5 # Seconds. + + #: Like Vim's `timeoutlen` option. This can be `None` or a float. For + #: instance, suppose that we have a key binding AB and a second key + #: binding A. If the uses presses A and then waits, we don't handle + #: this binding yet (unless it was marked 'eager'), because we don't + #: know what will follow. This timeout is the maximum amount of time + #: that we wait until we call the handlers anyway. Pass `None` to + #: disable this timeout. + self.timeoutlen = 1.0 + + #: The `Renderer` instance. + # Make sure that the same stdout is used, when a custom renderer has been passed. + self._merged_style = self._create_merged_style(include_default_pygments_style) + + self.renderer = Renderer( + self._merged_style, + self.output, + full_screen=full_screen, + mouse_support=mouse_support, + cpr_not_supported_callback=self.cpr_not_supported_callback, + ) + + #: Render counter. This one is increased every time the UI is rendered. + #: It can be used as a key for caching certain information during one + #: rendering. + self.render_counter = 0 + + # Invalidate flag. When 'True', a repaint has been scheduled. + self._invalidated = False + self._invalidate_events: List[ + Event[object] + ] = [] # Collection of 'invalidate' Event objects. + self._last_redraw_time = 0.0 # Unix timestamp of last redraw. Used when + # `min_redraw_interval` is given. + + #: The `InputProcessor` instance. + self.key_processor = KeyProcessor(_CombinedRegistry(self)) + + # If `run_in_terminal` was called. This will point to a `Future` what will be + # set at the point when the previous run finishes. + self._running_in_terminal = False + self._running_in_terminal_f: Optional[Future[None]] = None + + # Trigger initialize callback. + self.reset() + + def _create_merged_style(self, include_default_pygments_style: Filter) -> BaseStyle: + """ + Create a `Style` object that merges the default UI style, the default + pygments style, and the custom user style. + """ + dummy_style = DummyStyle() + pygments_style = default_pygments_style() + + @DynamicStyle + def conditional_pygments_style() -> BaseStyle: + if include_default_pygments_style(): + return pygments_style + else: + return dummy_style + + return merge_styles( + [ + default_ui_style(), + conditional_pygments_style, + DynamicStyle(lambda: self.style), + ] + ) + + @property + def color_depth(self) -> ColorDepth: + """ + The active :class:`.ColorDepth`. + + The current value is determined as follows: + + - If a color depth was given explicitly to this application, use that + value. + - Otherwise, fall back to the color depth that is reported by the + :class:`.Output` implementation. If the :class:`.Output` class was + created using `output.defaults.create_output`, then this value is + coming from the $PROMPT_TOOLKIT_COLOR_DEPTH environment variable. + """ + depth = self._color_depth + + if callable(depth): + depth = depth() + + if depth is None: + depth = self.output.get_default_color_depth() + + return depth + + @property + def current_buffer(self) -> Buffer: + """ + The currently focused :class:`~.Buffer`. + + (This returns a dummy :class:`.Buffer` when none of the actual buffers + has the focus. In this case, it's really not practical to check for + `None` values or catch exceptions every time.) + """ + return self.layout.current_buffer or Buffer( + name="dummy-buffer" + ) # Dummy buffer. + + @property + def current_search_state(self) -> SearchState: + """ + Return the current :class:`.SearchState`. (The one for the focused + :class:`.BufferControl`.) + """ + ui_control = self.layout.current_control + if isinstance(ui_control, BufferControl): + return ui_control.search_state + else: + return SearchState() # Dummy search state. (Don't return None!) + + def reset(self) -> None: + """ + Reset everything, for reading the next input. + """ + # Notice that we don't reset the buffers. (This happens just before + # returning, and when we have multiple buffers, we clearly want the + # content in the other buffers to remain unchanged between several + # calls of `run`. (And the same is true for the focus stack.) + + self.exit_style = "" + + self.background_tasks: List[Task[None]] = [] + + self.renderer.reset() + self.key_processor.reset() + self.layout.reset() + self.vi_state.reset() + self.emacs_state.reset() + + # Trigger reset event. + self.on_reset.fire() + + # Make sure that we have a 'focusable' widget focused. + # (The `Layout` class can't determine this.) + layout = self.layout + + if not layout.current_control.is_focusable(): + for w in layout.find_all_windows(): + if w.content.is_focusable(): + layout.current_window = w + break + + def invalidate(self) -> None: + """ + Thread safe way of sending a repaint trigger to the input event loop. + """ + if not self._is_running: + # Don't schedule a redraw if we're not running. + # Otherwise, `get_event_loop()` in `call_soon_threadsafe` can fail. + # See: https://github.com/dbcli/mycli/issues/797 + return + + # `invalidate()` called if we don't have a loop yet (not running?), or + # after the event loop was closed. + if self.loop is None or self.loop.is_closed(): + return + + # Never schedule a second redraw, when a previous one has not yet been + # executed. (This should protect against other threads calling + # 'invalidate' many times, resulting in 100% CPU.) + if self._invalidated: + return + else: + self._invalidated = True + + # Trigger event. + self.loop.call_soon_threadsafe(self.on_invalidate.fire) + + def redraw() -> None: + self._invalidated = False + self._redraw() + + def schedule_redraw() -> None: + call_soon_threadsafe( + redraw, max_postpone_time=self.max_render_postpone_time, loop=self.loop + ) + + if self.min_redraw_interval: + # When a minimum redraw interval is set, wait minimum this amount + # of time between redraws. + diff = time.time() - self._last_redraw_time + if diff < self.min_redraw_interval: + + async def redraw_in_future() -> None: + await sleep(cast(float, self.min_redraw_interval) - diff) + schedule_redraw() + + self.loop.call_soon_threadsafe( + lambda: self.create_background_task(redraw_in_future()) + ) + else: + schedule_redraw() + else: + schedule_redraw() + + @property + def invalidated(self) -> bool: + "True when a redraw operation has been scheduled." + return self._invalidated + + def _redraw(self, render_as_done: bool = False) -> None: + """ + Render the command line again. (Not thread safe!) (From other threads, + or if unsure, use :meth:`.Application.invalidate`.) + + :param render_as_done: make sure to put the cursor after the UI. + """ + + def run_in_context() -> None: + # Only draw when no sub application was started. + if self._is_running and not self._running_in_terminal: + if self.min_redraw_interval: + self._last_redraw_time = time.time() + + # Render + self.render_counter += 1 + self.before_render.fire() + + if render_as_done: + if self.erase_when_done: + self.renderer.erase() + else: + # Draw in 'done' state and reset renderer. + self.renderer.render(self, self.layout, is_done=render_as_done) + else: + self.renderer.render(self, self.layout) + + self.layout.update_parents_relations() + + # Fire render event. + self.after_render.fire() + + self._update_invalidate_events() + + # NOTE: We want to make sure this Application is the active one. The + # invalidate function is often called from a context where this + # application is not the active one. (Like the + # `PromptSession._auto_refresh_context`). + # We copy the context in case the context was already active, to + # prevent RuntimeErrors. (The rendering is not supposed to change + # any context variables.) + if self.context is not None: + self.context.copy().run(run_in_context) + + def _start_auto_refresh_task(self) -> None: + """ + Start a while/true loop in the background for automatic invalidation of + the UI. + """ + if self.refresh_interval is not None and self.refresh_interval != 0: + + async def auto_refresh(refresh_interval: float) -> None: + while True: + await sleep(refresh_interval) + self.invalidate() + + self.create_background_task(auto_refresh(self.refresh_interval)) + + def _update_invalidate_events(self) -> None: + """ + Make sure to attach 'invalidate' handlers to all invalidate events in + the UI. + """ + # Remove all the original event handlers. (Components can be removed + # from the UI.) + for ev in self._invalidate_events: + ev -= self._invalidate_handler + + # Gather all new events. + # (All controls are able to invalidate themselves.) + def gather_events() -> Iterable[Event[object]]: + for c in self.layout.find_all_controls(): + for ev in c.get_invalidate_events(): + yield ev + + self._invalidate_events = list(gather_events()) + + for ev in self._invalidate_events: + ev += self._invalidate_handler + + def _invalidate_handler(self, sender: object) -> None: + """ + Handler for invalidate events coming from UIControls. + + (This handles the difference in signature between event handler and + `self.invalidate`. It also needs to be a method -not a nested + function-, so that we can remove it again .) + """ + self.invalidate() + + def _on_resize(self) -> None: + """ + When the window size changes, we erase the current output and request + again the cursor position. When the CPR answer arrives, the output is + drawn again. + """ + # Erase, request position (when cursor is at the start position) + # and redraw again. -- The order is important. + self.renderer.erase(leave_alternate_screen=False) + self._request_absolute_cursor_position() + self._redraw() + + def _pre_run(self, pre_run: Optional[Callable[[], None]] = None) -> None: + """ + Called during `run`. + + `self.future` should be set to the new future at the point where this + is called in order to avoid data races. `pre_run` can be used to set a + `threading.Event` to synchronize with UI termination code, running in + another thread that would call `Application.exit`. (See the progress + bar code for an example.) + """ + if pre_run: + pre_run() + + # Process registered "pre_run_callables" and clear list. + for c in self.pre_run_callables: + c() + del self.pre_run_callables[:] + + async def run_async( + self, + pre_run: Optional[Callable[[], None]] = None, + set_exception_handler: bool = True, handle_sigint: bool = True, slow_callback_duration: float = 0.5, - ) -> _AppResult: - """ - Run the prompt_toolkit :class:`~prompt_toolkit.application.Application` - until :meth:`~prompt_toolkit.application.Application.exit` has been - called. Return the value that was passed to - :meth:`~prompt_toolkit.application.Application.exit`. - - This is the main entry point for a prompt_toolkit - :class:`~prompt_toolkit.application.Application` and usually the only - place where the event loop is actually running. - - :param pre_run: Optional callable, which is called right after the - "reset" of the application. - :param set_exception_handler: When set, in case of an exception, go out - of the alternate screen and hide the application, display the - exception, and wait for the user to press ENTER. + ) -> _AppResult: + """ + Run the prompt_toolkit :class:`~prompt_toolkit.application.Application` + until :meth:`~prompt_toolkit.application.Application.exit` has been + called. Return the value that was passed to + :meth:`~prompt_toolkit.application.Application.exit`. + + This is the main entry point for a prompt_toolkit + :class:`~prompt_toolkit.application.Application` and usually the only + place where the event loop is actually running. + + :param pre_run: Optional callable, which is called right after the + "reset" of the application. + :param set_exception_handler: When set, in case of an exception, go out + of the alternate screen and hide the application, display the + exception, and wait for the user to press ENTER. :param handle_sigint: Handle SIGINT signal if possible. This will call the `<sigint>` key binding when a SIGINT is received. (This only works in the main thread.) @@ -661,9 +661,9 @@ class Application(Generic[_AppResult]): default of `0.1` is sometimes not sufficient on a slow system, because exceptionally, the drawing of the app, which happens in the event loop, can take a bit longer from time to time. - """ - assert not self._is_running, "Application is already running." - + """ + assert not self._is_running, "Application is already running." + if not in_main_thread() or is_windows(): # Handling signals in other threads is not supported. # Also on Windows, `add_signal_handler(signal.SIGINT, ...)` raises @@ -671,130 +671,130 @@ class Application(Generic[_AppResult]): # See: https://github.com/prompt-toolkit/python-prompt-toolkit/issues/1553 handle_sigint = False - async def _run_async() -> _AppResult: - "Coroutine." - loop = get_event_loop() - f = loop.create_future() - self.future = f # XXX: make sure to set this before calling '_redraw'. - self.loop = loop - self.context = contextvars.copy_context() - - # Counter for cancelling 'flush' timeouts. Every time when a key is - # pressed, we start a 'flush' timer for flushing our escape key. But - # when any subsequent input is received, a new timer is started and - # the current timer will be ignored. - flush_task: Optional[asyncio.Task[None]] = None - - # Reset. - # (`self.future` needs to be set when `pre_run` is called.) - self.reset() - self._pre_run(pre_run) - - # Feed type ahead input first. - self.key_processor.feed_multiple(get_typeahead(self.input)) - self.key_processor.process_keys() - - def read_from_input() -> None: - nonlocal flush_task - - # Ignore when we aren't running anymore. This callback will - # removed from the loop next time. (It could be that it was - # still in the 'tasks' list of the loop.) - # Except: if we need to process incoming CPRs. - if not self._is_running and not self.renderer.waiting_for_cpr: - return - - # Get keys from the input object. - keys = self.input.read_keys() - - # Feed to key processor. - self.key_processor.feed_multiple(keys) - self.key_processor.process_keys() - - # Quit when the input stream was closed. - if self.input.closed: - if not f.done(): - f.set_exception(EOFError) - else: - # Automatically flush keys. - if flush_task: - flush_task.cancel() - flush_task = self.create_background_task(auto_flush_input()) - - async def auto_flush_input() -> None: - # Flush input after timeout. - # (Used for flushing the enter key.) - # This sleep can be cancelled, in that case we won't flush yet. - await sleep(self.ttimeoutlen) - flush_input() - - def flush_input() -> None: - if not self.is_done: - # Get keys, and feed to key processor. - keys = self.input.flush_keys() - self.key_processor.feed_multiple(keys) - self.key_processor.process_keys() - - if self.input.closed: - f.set_exception(EOFError) - - # Enter raw mode, attach input and attach WINCH event handler. - with self.input.raw_mode(), self.input.attach( - read_from_input - ), attach_winch_signal_handler(self._on_resize): - - # Draw UI. - self._request_absolute_cursor_position() - self._redraw() - self._start_auto_refresh_task() - + async def _run_async() -> _AppResult: + "Coroutine." + loop = get_event_loop() + f = loop.create_future() + self.future = f # XXX: make sure to set this before calling '_redraw'. + self.loop = loop + self.context = contextvars.copy_context() + + # Counter for cancelling 'flush' timeouts. Every time when a key is + # pressed, we start a 'flush' timer for flushing our escape key. But + # when any subsequent input is received, a new timer is started and + # the current timer will be ignored. + flush_task: Optional[asyncio.Task[None]] = None + + # Reset. + # (`self.future` needs to be set when `pre_run` is called.) + self.reset() + self._pre_run(pre_run) + + # Feed type ahead input first. + self.key_processor.feed_multiple(get_typeahead(self.input)) + self.key_processor.process_keys() + + def read_from_input() -> None: + nonlocal flush_task + + # Ignore when we aren't running anymore. This callback will + # removed from the loop next time. (It could be that it was + # still in the 'tasks' list of the loop.) + # Except: if we need to process incoming CPRs. + if not self._is_running and not self.renderer.waiting_for_cpr: + return + + # Get keys from the input object. + keys = self.input.read_keys() + + # Feed to key processor. + self.key_processor.feed_multiple(keys) + self.key_processor.process_keys() + + # Quit when the input stream was closed. + if self.input.closed: + if not f.done(): + f.set_exception(EOFError) + else: + # Automatically flush keys. + if flush_task: + flush_task.cancel() + flush_task = self.create_background_task(auto_flush_input()) + + async def auto_flush_input() -> None: + # Flush input after timeout. + # (Used for flushing the enter key.) + # This sleep can be cancelled, in that case we won't flush yet. + await sleep(self.ttimeoutlen) + flush_input() + + def flush_input() -> None: + if not self.is_done: + # Get keys, and feed to key processor. + keys = self.input.flush_keys() + self.key_processor.feed_multiple(keys) + self.key_processor.process_keys() + + if self.input.closed: + f.set_exception(EOFError) + + # Enter raw mode, attach input and attach WINCH event handler. + with self.input.raw_mode(), self.input.attach( + read_from_input + ), attach_winch_signal_handler(self._on_resize): + + # Draw UI. + self._request_absolute_cursor_position() + self._redraw() + self._start_auto_refresh_task() + self.create_background_task(self._poll_output_size()) - # Wait for UI to finish. - try: - result = await f - finally: - # In any case, when the application finishes. - # (Successful, or because of an error.) - try: - self._redraw(render_as_done=True) - finally: - # _redraw has a good chance to fail if it calls widgets - # with bad code. Make sure to reset the renderer - # anyway. - self.renderer.reset() - - # Unset `is_running`, this ensures that possibly - # scheduled draws won't paint during the following - # yield. - self._is_running = False - - # Detach event handlers for invalidate events. - # (Important when a UIControl is embedded in multiple - # applications, like ptterm in pymux. An invalidate - # should not trigger a repaint in terminated - # applications.) - for ev in self._invalidate_events: - ev -= self._invalidate_handler - self._invalidate_events = [] - - # Wait for CPR responses. - if self.output.responds_to_cpr: - await self.renderer.wait_for_cpr_responses() - - # Wait for the run-in-terminals to terminate. - previous_run_in_terminal_f = self._running_in_terminal_f - - if previous_run_in_terminal_f: - await previous_run_in_terminal_f - - # Store unprocessed input as typeahead for next time. - store_typeahead(self.input, self.key_processor.empty_queue()) - - return cast(_AppResult, result) - - async def _run_async2() -> _AppResult: - self._is_running = True + # Wait for UI to finish. + try: + result = await f + finally: + # In any case, when the application finishes. + # (Successful, or because of an error.) + try: + self._redraw(render_as_done=True) + finally: + # _redraw has a good chance to fail if it calls widgets + # with bad code. Make sure to reset the renderer + # anyway. + self.renderer.reset() + + # Unset `is_running`, this ensures that possibly + # scheduled draws won't paint during the following + # yield. + self._is_running = False + + # Detach event handlers for invalidate events. + # (Important when a UIControl is embedded in multiple + # applications, like ptterm in pymux. An invalidate + # should not trigger a repaint in terminated + # applications.) + for ev in self._invalidate_events: + ev -= self._invalidate_handler + self._invalidate_events = [] + + # Wait for CPR responses. + if self.output.responds_to_cpr: + await self.renderer.wait_for_cpr_responses() + + # Wait for the run-in-terminals to terminate. + previous_run_in_terminal_f = self._running_in_terminal_f + + if previous_run_in_terminal_f: + await previous_run_in_terminal_f + + # Store unprocessed input as typeahead for next time. + store_typeahead(self.input, self.key_processor.empty_queue()) + + return cast(_AppResult, result) + + async def _run_async2() -> _AppResult: + self._is_running = True try: # Make sure to set `_invalidated` to `False` to begin with, # otherwise we're not going to paint anything. This can happen if @@ -802,9 +802,9 @@ class Application(Generic[_AppResult]): # paint was scheduled using `call_soon_threadsafe` with # `max_postpone_time`. self._invalidated = False - + loop = get_event_loop() - + if handle_sigint: loop.add_signal_handler( signal.SIGINT, @@ -812,15 +812,15 @@ class Application(Generic[_AppResult]): self.key_processor.send_sigint ), ) - + if set_exception_handler: previous_exc_handler = loop.get_exception_handler() loop.set_exception_handler(self._handle_exception) - + # Set slow_callback_duration. original_slow_callback_duration = loop.slow_callback_duration loop.slow_callback_duration = slow_callback_duration - + try: with set_app(self), self._enable_breakpointhook(): try: @@ -831,7 +831,7 @@ class Application(Generic[_AppResult]): # `KeyboardInterrupt`, we still want to wait for the # background tasks. await self.cancel_and_wait_for_background_tasks() - + # Also remove the Future again. (This brings the # application back to its initial state, where it also # doesn't have a Future.) @@ -848,584 +848,584 @@ class Application(Generic[_AppResult]): # Reset slow_callback_duration. loop.slow_callback_duration = original_slow_callback_duration - finally: + finally: # Set the `_is_running` flag to `False`. Normally this happened # already in the finally block in `run_async` above, but in # case of exceptions, that's not always the case. self._is_running = False - - return await _run_async2() - - def run( - self, - pre_run: Optional[Callable[[], None]] = None, - set_exception_handler: bool = True, + + return await _run_async2() + + def run( + self, + pre_run: Optional[Callable[[], None]] = None, + set_exception_handler: bool = True, handle_sigint: bool = True, - in_thread: bool = False, - ) -> _AppResult: - """ - A blocking 'run' call that waits until the UI is finished. - - This will start the current asyncio event loop. If no loop is set for - the current thread, then it will create a new loop. If a new loop was - created, this won't close the new loop (if `in_thread=False`). - - :param pre_run: Optional callable, which is called right after the - "reset" of the application. - :param set_exception_handler: When set, in case of an exception, go out - of the alternate screen and hide the application, display the - exception, and wait for the user to press ENTER. - :param in_thread: When true, run the application in a background - thread, and block the current thread until the application - terminates. This is useful if we need to be sure the application - won't use the current event loop (asyncio does not support nested - event loops). A new event loop will be created in this background - thread, and that loop will also be closed when the background - thread terminates. When this is used, it's especially important to - make sure that all asyncio background tasks are managed through - `get_appp().create_background_task()`, so that unfinished tasks are - properly cancelled before the event loop is closed. This is used - for instance in ptpython. + in_thread: bool = False, + ) -> _AppResult: + """ + A blocking 'run' call that waits until the UI is finished. + + This will start the current asyncio event loop. If no loop is set for + the current thread, then it will create a new loop. If a new loop was + created, this won't close the new loop (if `in_thread=False`). + + :param pre_run: Optional callable, which is called right after the + "reset" of the application. + :param set_exception_handler: When set, in case of an exception, go out + of the alternate screen and hide the application, display the + exception, and wait for the user to press ENTER. + :param in_thread: When true, run the application in a background + thread, and block the current thread until the application + terminates. This is useful if we need to be sure the application + won't use the current event loop (asyncio does not support nested + event loops). A new event loop will be created in this background + thread, and that loop will also be closed when the background + thread terminates. When this is used, it's especially important to + make sure that all asyncio background tasks are managed through + `get_appp().create_background_task()`, so that unfinished tasks are + properly cancelled before the event loop is closed. This is used + for instance in ptpython. :param handle_sigint: Handle SIGINT signal. Call the key binding for `Keys.SIGINT`. (This only works in the main thread.) - """ - if in_thread: - result: _AppResult - exception: Optional[BaseException] = None - - def run_in_thread() -> None: - nonlocal result, exception - try: - result = self.run( + """ + if in_thread: + result: _AppResult + exception: Optional[BaseException] = None + + def run_in_thread() -> None: + nonlocal result, exception + try: + result = self.run( pre_run=pre_run, set_exception_handler=set_exception_handler, # Signal handling only works in the main thread. handle_sigint=False, - ) - except BaseException as e: - exception = e - finally: - # Make sure to close the event loop in this thread. Running - # the application creates a new loop (because we're in - # another thread), but it doesn't get closed automatically - # (also not by the garbage collector). - loop = get_event_loop() - loop.run_until_complete(loop.shutdown_asyncgens()) - loop.close() - - thread = threading.Thread(target=run_in_thread) - thread.start() - thread.join() - - if exception is not None: - raise exception - return result - - # We don't create a new event loop by default, because we want to be - # sure that when this is called multiple times, each call of `run()` - # goes through the same event loop. This way, users can schedule - # background-tasks that keep running across multiple prompts. - try: - loop = get_event_loop() - except RuntimeError: - # Possibly we are not running in the main thread, where no event - # loop is set by default. Or somebody called `asyncio.run()` - # before, which closes the existing event loop. We can create a new - # loop. - loop = new_event_loop() - set_event_loop(loop) - - return loop.run_until_complete( - self.run_async(pre_run=pre_run, set_exception_handler=set_exception_handler) - ) - - def _handle_exception( - self, loop: AbstractEventLoop, context: Dict[str, Any] - ) -> None: - """ - Handler for event loop exceptions. - This will print the exception, using run_in_terminal. - """ - # For Python 2: we have to get traceback at this point, because - # we're still in the 'except:' block of the event loop where the - # traceback is still available. Moving this code in the - # 'print_exception' coroutine will loose the exception. - tb = get_traceback_from_context(context) - formatted_tb = "".join(format_tb(tb)) - - async def in_term() -> None: - async with in_terminal(): - # Print output. Similar to 'loop.default_exception_handler', - # but don't use logger. (This works better on Python 2.) - print("\nUnhandled exception in event loop:") - print(formatted_tb) - print("Exception %s" % (context.get("exception"),)) - - await _do_wait_for_enter("Press ENTER to continue...") - - ensure_future(in_term()) - - @contextmanager - def _enable_breakpointhook(self) -> Generator[None, None, None]: - """ - Install our custom breakpointhook for the duration of this context - manager. (We will only install the hook if no other custom hook was - set.) - """ - if sys.version_info >= (3, 7) and sys.breakpointhook == sys.__breakpointhook__: - sys.breakpointhook = self._breakpointhook - - try: - yield - finally: - sys.breakpointhook = sys.__breakpointhook__ - else: - yield - - def _breakpointhook(self, *a: object, **kw: object) -> None: - """ - Breakpointhook which uses PDB, but ensures that the application is - hidden and input echoing is restored during each debugger dispatch. - """ - app = self - # Inline import on purpose. We don't want to import pdb, if not needed. - import pdb - from types import FrameType - - TraceDispatch = Callable[[FrameType, str, Any], Any] - - class CustomPdb(pdb.Pdb): - def trace_dispatch( - self, frame: FrameType, event: str, arg: Any - ) -> TraceDispatch: - # Hide application. - app.renderer.erase() - - # Detach input and dispatch to debugger. - with app.input.detach(): - with app.input.cooked_mode(): - return super().trace_dispatch(frame, event, arg) - - # Note: we don't render the application again here, because - # there's a good chance that there's a breakpoint on the next - # line. This paint/erase cycle would move the PDB prompt back - # to the middle of the screen. - - frame = sys._getframe().f_back - CustomPdb(stdout=sys.__stdout__).set_trace(frame) - - def create_background_task( - self, coroutine: Awaitable[None] - ) -> "asyncio.Task[None]": - """ - Start a background task (coroutine) for the running application. When - the `Application` terminates, unfinished background tasks will be - cancelled. - - If asyncio had nurseries like Trio, we would create a nursery in - `Application.run_async`, and run the given coroutine in that nursery. - - Not threadsafe. - """ - task = get_event_loop().create_task(coroutine) - self.background_tasks.append(task) - return task - - async def cancel_and_wait_for_background_tasks(self) -> None: - """ - Cancel all background tasks, and wait for the cancellation to be done. - If any of the background tasks raised an exception, this will also - propagate the exception. - - (If we had nurseries like Trio, this would be the `__aexit__` of a - nursery.) - """ - for task in self.background_tasks: - task.cancel() - - for task in self.background_tasks: - try: - await task - except CancelledError: - pass - - async def _poll_output_size(self) -> None: - """ - Coroutine for polling the terminal dimensions. - - Useful for situations where `attach_winch_signal_handler` is not sufficient: - - If we are not running in the main thread. - - On Windows. - """ - size: Optional[Size] = None - interval = self.terminal_size_polling_interval - - if interval is None: - return - - while True: - await asyncio.sleep(interval) - new_size = self.output.get_size() - - if size is not None and new_size != size: - self._on_resize() - size = new_size - - def cpr_not_supported_callback(self) -> None: - """ - Called when we don't receive the cursor position response in time. - """ - if not self.output.responds_to_cpr: - return # We know about this already. - - def in_terminal() -> None: - self.output.write( - "WARNING: your terminal doesn't support cursor position requests (CPR).\r\n" - ) - self.output.flush() - - run_in_terminal(in_terminal) - - @overload - def exit(self) -> None: - "Exit without arguments." - - @overload - def exit(self, *, result: _AppResult, style: str = "") -> None: - "Exit with `_AppResult`." - - @overload - def exit( - self, *, exception: Union[BaseException, Type[BaseException]], style: str = "" - ) -> None: - "Exit with exception." - - def exit( - self, - result: Optional[_AppResult] = None, - exception: Optional[Union[BaseException, Type[BaseException]]] = None, - style: str = "", - ) -> None: - """ - Exit application. - - .. note:: - - If `Application.exit` is called before `Application.run()` is - called, then the `Application` won't exit (because the - `Application.future` doesn't correspond to the current run). Use a - `pre_run` hook and an event to synchronize the closing if there's a - chance this can happen. - - :param result: Set this result for the application. - :param exception: Set this exception as the result for an application. For - a prompt, this is often `EOFError` or `KeyboardInterrupt`. - :param style: Apply this style on the whole content when quitting, - often this is 'class:exiting' for a prompt. (Used when - `erase_when_done` is not set.) - """ - assert result is None or exception is None - - if self.future is None: - raise Exception("Application is not running. Application.exit() failed.") - - if self.future.done(): - raise Exception("Return value already set. Application.exit() failed.") - - self.exit_style = style - - if exception is not None: - self.future.set_exception(exception) - else: - self.future.set_result(cast(_AppResult, result)) - - def _request_absolute_cursor_position(self) -> None: - """ - Send CPR request. - """ - # Note: only do this if the input queue is not empty, and a return - # value has not been set. Otherwise, we won't be able to read the - # response anyway. - if not self.key_processor.input_queue and not self.is_done: - self.renderer.request_absolute_cursor_position() - - async def run_system_command( - self, - command: str, - wait_for_enter: bool = True, - display_before_text: AnyFormattedText = "", - wait_text: str = "Press ENTER to continue...", - ) -> None: - """ - Run system command (While hiding the prompt. When finished, all the - output will scroll above the prompt.) - - :param command: Shell command to be executed. - :param wait_for_enter: FWait for the user to press enter, when the - command is finished. - :param display_before_text: If given, text to be displayed before the - command executes. - :return: A `Future` object. - """ - async with in_terminal(): - # Try to use the same input/output file descriptors as the one, - # used to run this application. - try: - input_fd = self.input.fileno() - except AttributeError: - input_fd = sys.stdin.fileno() - try: - output_fd = self.output.fileno() - except AttributeError: - output_fd = sys.stdout.fileno() - - # Run sub process. - def run_command() -> None: - self.print_text(display_before_text) - p = Popen(command, shell=True, stdin=input_fd, stdout=output_fd) - p.wait() - - await run_in_executor_with_context(run_command) - - # Wait for the user to press enter. - if wait_for_enter: - await _do_wait_for_enter(wait_text) - - def suspend_to_background(self, suspend_group: bool = True) -> None: - """ - (Not thread safe -- to be called from inside the key bindings.) - Suspend process. - - :param suspend_group: When true, suspend the whole process group. - (This is the default, and probably what you want.) - """ - # Only suspend when the operating system supports it. - # (Not on Windows.) - if _SIGTSTP is not None: - - def run() -> None: + ) + except BaseException as e: + exception = e + finally: + # Make sure to close the event loop in this thread. Running + # the application creates a new loop (because we're in + # another thread), but it doesn't get closed automatically + # (also not by the garbage collector). + loop = get_event_loop() + loop.run_until_complete(loop.shutdown_asyncgens()) + loop.close() + + thread = threading.Thread(target=run_in_thread) + thread.start() + thread.join() + + if exception is not None: + raise exception + return result + + # We don't create a new event loop by default, because we want to be + # sure that when this is called multiple times, each call of `run()` + # goes through the same event loop. This way, users can schedule + # background-tasks that keep running across multiple prompts. + try: + loop = get_event_loop() + except RuntimeError: + # Possibly we are not running in the main thread, where no event + # loop is set by default. Or somebody called `asyncio.run()` + # before, which closes the existing event loop. We can create a new + # loop. + loop = new_event_loop() + set_event_loop(loop) + + return loop.run_until_complete( + self.run_async(pre_run=pre_run, set_exception_handler=set_exception_handler) + ) + + def _handle_exception( + self, loop: AbstractEventLoop, context: Dict[str, Any] + ) -> None: + """ + Handler for event loop exceptions. + This will print the exception, using run_in_terminal. + """ + # For Python 2: we have to get traceback at this point, because + # we're still in the 'except:' block of the event loop where the + # traceback is still available. Moving this code in the + # 'print_exception' coroutine will loose the exception. + tb = get_traceback_from_context(context) + formatted_tb = "".join(format_tb(tb)) + + async def in_term() -> None: + async with in_terminal(): + # Print output. Similar to 'loop.default_exception_handler', + # but don't use logger. (This works better on Python 2.) + print("\nUnhandled exception in event loop:") + print(formatted_tb) + print("Exception %s" % (context.get("exception"),)) + + await _do_wait_for_enter("Press ENTER to continue...") + + ensure_future(in_term()) + + @contextmanager + def _enable_breakpointhook(self) -> Generator[None, None, None]: + """ + Install our custom breakpointhook for the duration of this context + manager. (We will only install the hook if no other custom hook was + set.) + """ + if sys.version_info >= (3, 7) and sys.breakpointhook == sys.__breakpointhook__: + sys.breakpointhook = self._breakpointhook + + try: + yield + finally: + sys.breakpointhook = sys.__breakpointhook__ + else: + yield + + def _breakpointhook(self, *a: object, **kw: object) -> None: + """ + Breakpointhook which uses PDB, but ensures that the application is + hidden and input echoing is restored during each debugger dispatch. + """ + app = self + # Inline import on purpose. We don't want to import pdb, if not needed. + import pdb + from types import FrameType + + TraceDispatch = Callable[[FrameType, str, Any], Any] + + class CustomPdb(pdb.Pdb): + def trace_dispatch( + self, frame: FrameType, event: str, arg: Any + ) -> TraceDispatch: + # Hide application. + app.renderer.erase() + + # Detach input and dispatch to debugger. + with app.input.detach(): + with app.input.cooked_mode(): + return super().trace_dispatch(frame, event, arg) + + # Note: we don't render the application again here, because + # there's a good chance that there's a breakpoint on the next + # line. This paint/erase cycle would move the PDB prompt back + # to the middle of the screen. + + frame = sys._getframe().f_back + CustomPdb(stdout=sys.__stdout__).set_trace(frame) + + def create_background_task( + self, coroutine: Awaitable[None] + ) -> "asyncio.Task[None]": + """ + Start a background task (coroutine) for the running application. When + the `Application` terminates, unfinished background tasks will be + cancelled. + + If asyncio had nurseries like Trio, we would create a nursery in + `Application.run_async`, and run the given coroutine in that nursery. + + Not threadsafe. + """ + task = get_event_loop().create_task(coroutine) + self.background_tasks.append(task) + return task + + async def cancel_and_wait_for_background_tasks(self) -> None: + """ + Cancel all background tasks, and wait for the cancellation to be done. + If any of the background tasks raised an exception, this will also + propagate the exception. + + (If we had nurseries like Trio, this would be the `__aexit__` of a + nursery.) + """ + for task in self.background_tasks: + task.cancel() + + for task in self.background_tasks: + try: + await task + except CancelledError: + pass + + async def _poll_output_size(self) -> None: + """ + Coroutine for polling the terminal dimensions. + + Useful for situations where `attach_winch_signal_handler` is not sufficient: + - If we are not running in the main thread. + - On Windows. + """ + size: Optional[Size] = None + interval = self.terminal_size_polling_interval + + if interval is None: + return + + while True: + await asyncio.sleep(interval) + new_size = self.output.get_size() + + if size is not None and new_size != size: + self._on_resize() + size = new_size + + def cpr_not_supported_callback(self) -> None: + """ + Called when we don't receive the cursor position response in time. + """ + if not self.output.responds_to_cpr: + return # We know about this already. + + def in_terminal() -> None: + self.output.write( + "WARNING: your terminal doesn't support cursor position requests (CPR).\r\n" + ) + self.output.flush() + + run_in_terminal(in_terminal) + + @overload + def exit(self) -> None: + "Exit without arguments." + + @overload + def exit(self, *, result: _AppResult, style: str = "") -> None: + "Exit with `_AppResult`." + + @overload + def exit( + self, *, exception: Union[BaseException, Type[BaseException]], style: str = "" + ) -> None: + "Exit with exception." + + def exit( + self, + result: Optional[_AppResult] = None, + exception: Optional[Union[BaseException, Type[BaseException]]] = None, + style: str = "", + ) -> None: + """ + Exit application. + + .. note:: + + If `Application.exit` is called before `Application.run()` is + called, then the `Application` won't exit (because the + `Application.future` doesn't correspond to the current run). Use a + `pre_run` hook and an event to synchronize the closing if there's a + chance this can happen. + + :param result: Set this result for the application. + :param exception: Set this exception as the result for an application. For + a prompt, this is often `EOFError` or `KeyboardInterrupt`. + :param style: Apply this style on the whole content when quitting, + often this is 'class:exiting' for a prompt. (Used when + `erase_when_done` is not set.) + """ + assert result is None or exception is None + + if self.future is None: + raise Exception("Application is not running. Application.exit() failed.") + + if self.future.done(): + raise Exception("Return value already set. Application.exit() failed.") + + self.exit_style = style + + if exception is not None: + self.future.set_exception(exception) + else: + self.future.set_result(cast(_AppResult, result)) + + def _request_absolute_cursor_position(self) -> None: + """ + Send CPR request. + """ + # Note: only do this if the input queue is not empty, and a return + # value has not been set. Otherwise, we won't be able to read the + # response anyway. + if not self.key_processor.input_queue and not self.is_done: + self.renderer.request_absolute_cursor_position() + + async def run_system_command( + self, + command: str, + wait_for_enter: bool = True, + display_before_text: AnyFormattedText = "", + wait_text: str = "Press ENTER to continue...", + ) -> None: + """ + Run system command (While hiding the prompt. When finished, all the + output will scroll above the prompt.) + + :param command: Shell command to be executed. + :param wait_for_enter: FWait for the user to press enter, when the + command is finished. + :param display_before_text: If given, text to be displayed before the + command executes. + :return: A `Future` object. + """ + async with in_terminal(): + # Try to use the same input/output file descriptors as the one, + # used to run this application. + try: + input_fd = self.input.fileno() + except AttributeError: + input_fd = sys.stdin.fileno() + try: + output_fd = self.output.fileno() + except AttributeError: + output_fd = sys.stdout.fileno() + + # Run sub process. + def run_command() -> None: + self.print_text(display_before_text) + p = Popen(command, shell=True, stdin=input_fd, stdout=output_fd) + p.wait() + + await run_in_executor_with_context(run_command) + + # Wait for the user to press enter. + if wait_for_enter: + await _do_wait_for_enter(wait_text) + + def suspend_to_background(self, suspend_group: bool = True) -> None: + """ + (Not thread safe -- to be called from inside the key bindings.) + Suspend process. + + :param suspend_group: When true, suspend the whole process group. + (This is the default, and probably what you want.) + """ + # Only suspend when the operating system supports it. + # (Not on Windows.) + if _SIGTSTP is not None: + + def run() -> None: signal = cast(int, _SIGTSTP) - # Send `SIGTSTP` to own process. - # This will cause it to suspend. - - # Usually we want the whole process group to be suspended. This - # handles the case when input is piped from another process. - if suspend_group: + # Send `SIGTSTP` to own process. + # This will cause it to suspend. + + # Usually we want the whole process group to be suspended. This + # handles the case when input is piped from another process. + if suspend_group: os.kill(0, signal) - else: + else: os.kill(os.getpid(), signal) - - run_in_terminal(run) - - def print_text( - self, text: AnyFormattedText, style: Optional[BaseStyle] = None - ) -> None: - """ - Print a list of (style_str, text) tuples to the output. - (When the UI is running, this method has to be called through - `run_in_terminal`, otherwise it will destroy the UI.) - - :param text: List of ``(style_str, text)`` tuples. - :param style: Style class to use. Defaults to the active style in the CLI. - """ - print_formatted_text( - output=self.output, - formatted_text=text, - style=style or self._merged_style, - color_depth=self.color_depth, - style_transformation=self.style_transformation, - ) - - @property - def is_running(self) -> bool: - "`True` when the application is currently active/running." - return self._is_running - - @property - def is_done(self) -> bool: - if self.future: - return self.future.done() - return False - - def get_used_style_strings(self) -> List[str]: - """ - Return a list of used style strings. This is helpful for debugging, and - for writing a new `Style`. - """ - attrs_for_style = self.renderer._attrs_for_style - - if attrs_for_style: - return sorted( - [ - re.sub(r"\s+", " ", style_str).strip() - for style_str in attrs_for_style.keys() - ] - ) - - return [] - - -class _CombinedRegistry(KeyBindingsBase): - """ - The `KeyBindings` of key bindings for a `Application`. - This merges the global key bindings with the one of the current user - control. - """ - - def __init__(self, app: Application[_AppResult]) -> None: - self.app = app - self._cache: SimpleCache[ - Tuple[Window, FrozenSet[UIControl]], KeyBindingsBase - ] = SimpleCache() - - @property - def _version(self) -> Hashable: - """Not needed - this object is not going to be wrapped in another - KeyBindings object.""" - raise NotImplementedError - - def bindings(self) -> List[Binding]: - """Not needed - this object is not going to be wrapped in another - KeyBindings object.""" - raise NotImplementedError - - def _create_key_bindings( - self, current_window: Window, other_controls: List[UIControl] - ) -> KeyBindingsBase: - """ - Create a `KeyBindings` object that merges the `KeyBindings` from the - `UIControl` with all the parent controls and the global key bindings. - """ - key_bindings = [] - collected_containers = set() - - # Collect key bindings from currently focused control and all parent - # controls. Don't include key bindings of container parent controls. - container: Container = current_window - while True: - collected_containers.add(container) - kb = container.get_key_bindings() - if kb is not None: - key_bindings.append(kb) - - if container.is_modal(): - break - - parent = self.app.layout.get_parent(container) - if parent is None: - break - else: - container = parent - - # Include global bindings (starting at the top-model container). - for c in walk(container): - if c not in collected_containers: - kb = c.get_key_bindings() - if kb is not None: - key_bindings.append(GlobalOnlyKeyBindings(kb)) - - # Add App key bindings - if self.app.key_bindings: - key_bindings.append(self.app.key_bindings) - - # Add mouse bindings. - key_bindings.append( - ConditionalKeyBindings( - self.app._page_navigation_bindings, - self.app.enable_page_navigation_bindings, - ) - ) - key_bindings.append(self.app._default_bindings) - - # Reverse this list. The current control's key bindings should come - # last. They need priority. - key_bindings = key_bindings[::-1] - - return merge_key_bindings(key_bindings) - - @property - def _key_bindings(self) -> KeyBindingsBase: - current_window = self.app.layout.current_window - other_controls = list(self.app.layout.find_all_controls()) - key = current_window, frozenset(other_controls) - - return self._cache.get( - key, lambda: self._create_key_bindings(current_window, other_controls) - ) - - def get_bindings_for_keys(self, keys: KeysTuple) -> List[Binding]: - return self._key_bindings.get_bindings_for_keys(keys) - - def get_bindings_starting_with_keys(self, keys: KeysTuple) -> List[Binding]: - return self._key_bindings.get_bindings_starting_with_keys(keys) - - -async def _do_wait_for_enter(wait_text: AnyFormattedText) -> None: - """ - Create a sub application to wait for the enter key press. - This has two advantages over using 'input'/'raw_input': - - This will share the same input/output I/O. - - This doesn't block the event loop. - """ - from prompt_toolkit.shortcuts import PromptSession - - key_bindings = KeyBindings() - - @key_bindings.add("enter") - def _ok(event: E) -> None: - event.app.exit() - - @key_bindings.add(Keys.Any) - def _ignore(event: E) -> None: - "Disallow typing." - pass - - session: PromptSession[None] = PromptSession( - message=wait_text, key_bindings=key_bindings - ) - await session.app.run_async() - - -@contextmanager -def attach_winch_signal_handler( - handler: Callable[[], None] -) -> Generator[None, None, None]: - """ - Attach the given callback as a WINCH signal handler within the context - manager. Restore the original signal handler when done. - - The `Application.run` method will register SIGWINCH, so that it will - properly repaint when the terminal window resizes. However, using - `run_in_terminal`, we can temporarily send an application to the - background, and run an other app in between, which will then overwrite the - SIGWINCH. This is why it's important to restore the handler when the app - terminates. - """ - # The tricky part here is that signals are registered in the Unix event - # loop with a wakeup fd, but another application could have registered - # signals using signal.signal directly. For now, the implementation is - # hard-coded for the `asyncio.unix_events._UnixSelectorEventLoop`. - - # No WINCH? Then don't do anything. - sigwinch = getattr(signal, "SIGWINCH", None) - if sigwinch is None or not in_main_thread(): - yield - return - - # Keep track of the previous handler. - # (Only UnixSelectorEventloop has `_signal_handlers`.) - loop = get_event_loop() - previous_winch_handler = getattr(loop, "_signal_handlers", {}).get(sigwinch) - - try: - loop.add_signal_handler(sigwinch, handler) - yield - finally: - # Restore the previous signal handler. - loop.remove_signal_handler(sigwinch) - if previous_winch_handler is not None: - loop.add_signal_handler( - sigwinch, - previous_winch_handler._callback, - *previous_winch_handler._args, - ) + + run_in_terminal(run) + + def print_text( + self, text: AnyFormattedText, style: Optional[BaseStyle] = None + ) -> None: + """ + Print a list of (style_str, text) tuples to the output. + (When the UI is running, this method has to be called through + `run_in_terminal`, otherwise it will destroy the UI.) + + :param text: List of ``(style_str, text)`` tuples. + :param style: Style class to use. Defaults to the active style in the CLI. + """ + print_formatted_text( + output=self.output, + formatted_text=text, + style=style or self._merged_style, + color_depth=self.color_depth, + style_transformation=self.style_transformation, + ) + + @property + def is_running(self) -> bool: + "`True` when the application is currently active/running." + return self._is_running + + @property + def is_done(self) -> bool: + if self.future: + return self.future.done() + return False + + def get_used_style_strings(self) -> List[str]: + """ + Return a list of used style strings. This is helpful for debugging, and + for writing a new `Style`. + """ + attrs_for_style = self.renderer._attrs_for_style + + if attrs_for_style: + return sorted( + [ + re.sub(r"\s+", " ", style_str).strip() + for style_str in attrs_for_style.keys() + ] + ) + + return [] + + +class _CombinedRegistry(KeyBindingsBase): + """ + The `KeyBindings` of key bindings for a `Application`. + This merges the global key bindings with the one of the current user + control. + """ + + def __init__(self, app: Application[_AppResult]) -> None: + self.app = app + self._cache: SimpleCache[ + Tuple[Window, FrozenSet[UIControl]], KeyBindingsBase + ] = SimpleCache() + + @property + def _version(self) -> Hashable: + """Not needed - this object is not going to be wrapped in another + KeyBindings object.""" + raise NotImplementedError + + def bindings(self) -> List[Binding]: + """Not needed - this object is not going to be wrapped in another + KeyBindings object.""" + raise NotImplementedError + + def _create_key_bindings( + self, current_window: Window, other_controls: List[UIControl] + ) -> KeyBindingsBase: + """ + Create a `KeyBindings` object that merges the `KeyBindings` from the + `UIControl` with all the parent controls and the global key bindings. + """ + key_bindings = [] + collected_containers = set() + + # Collect key bindings from currently focused control and all parent + # controls. Don't include key bindings of container parent controls. + container: Container = current_window + while True: + collected_containers.add(container) + kb = container.get_key_bindings() + if kb is not None: + key_bindings.append(kb) + + if container.is_modal(): + break + + parent = self.app.layout.get_parent(container) + if parent is None: + break + else: + container = parent + + # Include global bindings (starting at the top-model container). + for c in walk(container): + if c not in collected_containers: + kb = c.get_key_bindings() + if kb is not None: + key_bindings.append(GlobalOnlyKeyBindings(kb)) + + # Add App key bindings + if self.app.key_bindings: + key_bindings.append(self.app.key_bindings) + + # Add mouse bindings. + key_bindings.append( + ConditionalKeyBindings( + self.app._page_navigation_bindings, + self.app.enable_page_navigation_bindings, + ) + ) + key_bindings.append(self.app._default_bindings) + + # Reverse this list. The current control's key bindings should come + # last. They need priority. + key_bindings = key_bindings[::-1] + + return merge_key_bindings(key_bindings) + + @property + def _key_bindings(self) -> KeyBindingsBase: + current_window = self.app.layout.current_window + other_controls = list(self.app.layout.find_all_controls()) + key = current_window, frozenset(other_controls) + + return self._cache.get( + key, lambda: self._create_key_bindings(current_window, other_controls) + ) + + def get_bindings_for_keys(self, keys: KeysTuple) -> List[Binding]: + return self._key_bindings.get_bindings_for_keys(keys) + + def get_bindings_starting_with_keys(self, keys: KeysTuple) -> List[Binding]: + return self._key_bindings.get_bindings_starting_with_keys(keys) + + +async def _do_wait_for_enter(wait_text: AnyFormattedText) -> None: + """ + Create a sub application to wait for the enter key press. + This has two advantages over using 'input'/'raw_input': + - This will share the same input/output I/O. + - This doesn't block the event loop. + """ + from prompt_toolkit.shortcuts import PromptSession + + key_bindings = KeyBindings() + + @key_bindings.add("enter") + def _ok(event: E) -> None: + event.app.exit() + + @key_bindings.add(Keys.Any) + def _ignore(event: E) -> None: + "Disallow typing." + pass + + session: PromptSession[None] = PromptSession( + message=wait_text, key_bindings=key_bindings + ) + await session.app.run_async() + + +@contextmanager +def attach_winch_signal_handler( + handler: Callable[[], None] +) -> Generator[None, None, None]: + """ + Attach the given callback as a WINCH signal handler within the context + manager. Restore the original signal handler when done. + + The `Application.run` method will register SIGWINCH, so that it will + properly repaint when the terminal window resizes. However, using + `run_in_terminal`, we can temporarily send an application to the + background, and run an other app in between, which will then overwrite the + SIGWINCH. This is why it's important to restore the handler when the app + terminates. + """ + # The tricky part here is that signals are registered in the Unix event + # loop with a wakeup fd, but another application could have registered + # signals using signal.signal directly. For now, the implementation is + # hard-coded for the `asyncio.unix_events._UnixSelectorEventLoop`. + + # No WINCH? Then don't do anything. + sigwinch = getattr(signal, "SIGWINCH", None) + if sigwinch is None or not in_main_thread(): + yield + return + + # Keep track of the previous handler. + # (Only UnixSelectorEventloop has `_signal_handlers`.) + loop = get_event_loop() + previous_winch_handler = getattr(loop, "_signal_handlers", {}).get(sigwinch) + + try: + loop.add_signal_handler(sigwinch, handler) + yield + finally: + # Restore the previous signal handler. + loop.remove_signal_handler(sigwinch) + if previous_winch_handler is not None: + loop.add_signal_handler( + sigwinch, + previous_winch_handler._callback, + *previous_winch_handler._args, + ) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/application/current.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/application/current.py index d768a470d9..14eb7922c8 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/application/current.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/application/current.py @@ -1,174 +1,174 @@ -import sys -from contextlib import contextmanager -from typing import TYPE_CHECKING, Any, Generator, Optional - -try: - from contextvars import ContextVar -except ImportError: - from prompt_toolkit.eventloop.dummy_contextvars import ContextVar # type: ignore - -if TYPE_CHECKING: - from prompt_toolkit.input.base import Input - from prompt_toolkit.output.base import Output - - from .application import Application - -__all__ = [ - "AppSession", - "get_app_session", - "get_app", - "get_app_or_none", - "set_app", - "create_app_session", +import sys +from contextlib import contextmanager +from typing import TYPE_CHECKING, Any, Generator, Optional + +try: + from contextvars import ContextVar +except ImportError: + from prompt_toolkit.eventloop.dummy_contextvars import ContextVar # type: ignore + +if TYPE_CHECKING: + from prompt_toolkit.input.base import Input + from prompt_toolkit.output.base import Output + + from .application import Application + +__all__ = [ + "AppSession", + "get_app_session", + "get_app", + "get_app_or_none", + "set_app", + "create_app_session", "create_app_session_from_tty", -] - - -class AppSession: - """ - An AppSession is an interactive session, usually connected to one terminal. - Within one such session, interaction with many applications can happen, one - after the other. - - The input/output device is not supposed to change during one session. - - Warning: Always use the `create_app_session` function to create an - instance, so that it gets activated correctly. - - :param input: Use this as a default input for all applications - running in this session, unless an input is passed to the `Application` - explicitely. - :param output: Use this as a default output. - """ - - def __init__( - self, input: Optional["Input"] = None, output: Optional["Output"] = None - ) -> None: - - self._input = input - self._output = output - - # The application will be set dynamically by the `set_app` context - # manager. This is called in the application itself. - self.app: Optional["Application[Any]"] = None - - def __repr__(self) -> str: - return "AppSession(app=%r)" % (self.app,) - - @property - def input(self) -> "Input": - if self._input is None: - from prompt_toolkit.input.defaults import create_input - - self._input = create_input() - return self._input - - @property - def output(self) -> "Output": - if self._output is None: - from prompt_toolkit.output.defaults import create_output - - self._output = create_output() - return self._output - - -_current_app_session: ContextVar["AppSession"] = ContextVar( - "_current_app_session", default=AppSession() -) - - -def get_app_session() -> AppSession: - return _current_app_session.get() - - -def get_app() -> "Application[Any]": - """ - Get the current active (running) Application. - An :class:`.Application` is active during the - :meth:`.Application.run_async` call. - - We assume that there can only be one :class:`.Application` active at the - same time. There is only one terminal window, with only one stdin and - stdout. This makes the code significantly easier than passing around the - :class:`.Application` everywhere. - - If no :class:`.Application` is running, then return by default a - :class:`.DummyApplication`. For practical reasons, we prefer to not raise - an exception. This way, we don't have to check all over the place whether - an actual `Application` was returned. - - (For applications like pymux where we can have more than one `Application`, - we'll use a work-around to handle that.) - """ - session = _current_app_session.get() - if session.app is not None: - return session.app - - from .dummy import DummyApplication - - return DummyApplication() - - -def get_app_or_none() -> Optional["Application[Any]"]: - """ - Get the current active (running) Application, or return `None` if no - application is running. - """ - session = _current_app_session.get() - return session.app - - -@contextmanager -def set_app(app: "Application[Any]") -> Generator[None, None, None]: - """ - Context manager that sets the given :class:`.Application` active in an - `AppSession`. - - This should only be called by the `Application` itself. - The application will automatically be active while its running. If you want - the application to be active in other threads/coroutines, where that's not - the case, use `contextvars.copy_context()`, or use `Application.context` to - run it in the appropriate context. - """ - session = _current_app_session.get() - - previous_app = session.app - session.app = app - try: - yield - finally: - session.app = previous_app - - -@contextmanager -def create_app_session( - input: Optional["Input"] = None, output: Optional["Output"] = None -) -> Generator[AppSession, None, None]: - """ - Create a separate AppSession. - - This is useful if there can be multiple individual `AppSession`s going on. - Like in the case of an Telnet/SSH server. This functionality uses - contextvars and requires at least Python 3.7. - """ - if sys.version_info <= (3, 6): - raise RuntimeError("Application sessions require Python 3.7.") - - # If no input/output is specified, fall back to the current input/output, - # whatever that is. - if input is None: - input = get_app_session().input - if output is None: - output = get_app_session().output - - # Create new `AppSession` and activate. - session = AppSession(input=input, output=output) - - token = _current_app_session.set(session) - try: - yield session - finally: - _current_app_session.reset(token) +] + + +class AppSession: + """ + An AppSession is an interactive session, usually connected to one terminal. + Within one such session, interaction with many applications can happen, one + after the other. + + The input/output device is not supposed to change during one session. + + Warning: Always use the `create_app_session` function to create an + instance, so that it gets activated correctly. + + :param input: Use this as a default input for all applications + running in this session, unless an input is passed to the `Application` + explicitely. + :param output: Use this as a default output. + """ + + def __init__( + self, input: Optional["Input"] = None, output: Optional["Output"] = None + ) -> None: + + self._input = input + self._output = output + + # The application will be set dynamically by the `set_app` context + # manager. This is called in the application itself. + self.app: Optional["Application[Any]"] = None + + def __repr__(self) -> str: + return "AppSession(app=%r)" % (self.app,) + + @property + def input(self) -> "Input": + if self._input is None: + from prompt_toolkit.input.defaults import create_input + + self._input = create_input() + return self._input + + @property + def output(self) -> "Output": + if self._output is None: + from prompt_toolkit.output.defaults import create_output + + self._output = create_output() + return self._output + + +_current_app_session: ContextVar["AppSession"] = ContextVar( + "_current_app_session", default=AppSession() +) + + +def get_app_session() -> AppSession: + return _current_app_session.get() + + +def get_app() -> "Application[Any]": + """ + Get the current active (running) Application. + An :class:`.Application` is active during the + :meth:`.Application.run_async` call. + + We assume that there can only be one :class:`.Application` active at the + same time. There is only one terminal window, with only one stdin and + stdout. This makes the code significantly easier than passing around the + :class:`.Application` everywhere. + + If no :class:`.Application` is running, then return by default a + :class:`.DummyApplication`. For practical reasons, we prefer to not raise + an exception. This way, we don't have to check all over the place whether + an actual `Application` was returned. + + (For applications like pymux where we can have more than one `Application`, + we'll use a work-around to handle that.) + """ + session = _current_app_session.get() + if session.app is not None: + return session.app + + from .dummy import DummyApplication + + return DummyApplication() + + +def get_app_or_none() -> Optional["Application[Any]"]: + """ + Get the current active (running) Application, or return `None` if no + application is running. + """ + session = _current_app_session.get() + return session.app + + +@contextmanager +def set_app(app: "Application[Any]") -> Generator[None, None, None]: + """ + Context manager that sets the given :class:`.Application` active in an + `AppSession`. + + This should only be called by the `Application` itself. + The application will automatically be active while its running. If you want + the application to be active in other threads/coroutines, where that's not + the case, use `contextvars.copy_context()`, or use `Application.context` to + run it in the appropriate context. + """ + session = _current_app_session.get() + + previous_app = session.app + session.app = app + try: + yield + finally: + session.app = previous_app + + +@contextmanager +def create_app_session( + input: Optional["Input"] = None, output: Optional["Output"] = None +) -> Generator[AppSession, None, None]: + """ + Create a separate AppSession. + + This is useful if there can be multiple individual `AppSession`s going on. + Like in the case of an Telnet/SSH server. This functionality uses + contextvars and requires at least Python 3.7. + """ + if sys.version_info <= (3, 6): + raise RuntimeError("Application sessions require Python 3.7.") + + # If no input/output is specified, fall back to the current input/output, + # whatever that is. + if input is None: + input = get_app_session().input + if output is None: + output = get_app_session().output + + # Create new `AppSession` and activate. + session = AppSession(input=input, output=output) + + token = _current_app_session.set(session) + try: + yield session + finally: + _current_app_session.reset(token) @contextmanager diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/application/dummy.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/application/dummy.py index f5431aa02e..4e5e4aafdd 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/application/dummy.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/application/dummy.py @@ -1,51 +1,51 @@ -from typing import Callable, Optional - -from prompt_toolkit.formatted_text import AnyFormattedText -from prompt_toolkit.input import DummyInput -from prompt_toolkit.output import DummyOutput - -from .application import Application - -__all__ = [ - "DummyApplication", -] - - -class DummyApplication(Application[None]): - """ - When no :class:`.Application` is running, - :func:`.get_app` will run an instance of this :class:`.DummyApplication` instead. - """ - - def __init__(self) -> None: - super().__init__(output=DummyOutput(), input=DummyInput()) - - def run( - self, - pre_run: Optional[Callable[[], None]] = None, - set_exception_handler: bool = True, +from typing import Callable, Optional + +from prompt_toolkit.formatted_text import AnyFormattedText +from prompt_toolkit.input import DummyInput +from prompt_toolkit.output import DummyOutput + +from .application import Application + +__all__ = [ + "DummyApplication", +] + + +class DummyApplication(Application[None]): + """ + When no :class:`.Application` is running, + :func:`.get_app` will run an instance of this :class:`.DummyApplication` instead. + """ + + def __init__(self) -> None: + super().__init__(output=DummyOutput(), input=DummyInput()) + + def run( + self, + pre_run: Optional[Callable[[], None]] = None, + set_exception_handler: bool = True, handle_sigint: bool = True, - in_thread: bool = False, - ) -> None: - raise NotImplementedError("A DummyApplication is not supposed to run.") - - async def run_async( - self, - pre_run: Optional[Callable[[], None]] = None, - set_exception_handler: bool = True, + in_thread: bool = False, + ) -> None: + raise NotImplementedError("A DummyApplication is not supposed to run.") + + async def run_async( + self, + pre_run: Optional[Callable[[], None]] = None, + set_exception_handler: bool = True, handle_sigint: bool = True, slow_callback_duration: float = 0.5, - ) -> None: - raise NotImplementedError("A DummyApplication is not supposed to run.") - - async def run_system_command( - self, - command: str, - wait_for_enter: bool = True, - display_before_text: AnyFormattedText = "", - wait_text: str = "", - ) -> None: - raise NotImplementedError - - def suspend_to_background(self, suspend_group: bool = True) -> None: - raise NotImplementedError + ) -> None: + raise NotImplementedError("A DummyApplication is not supposed to run.") + + async def run_system_command( + self, + command: str, + wait_for_enter: bool = True, + display_before_text: AnyFormattedText = "", + wait_text: str = "", + ) -> None: + raise NotImplementedError + + def suspend_to_background(self, suspend_group: bool = True) -> None: + raise NotImplementedError diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/application/run_in_terminal.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/application/run_in_terminal.py index ea2bfba452..d5ef8aafa3 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/application/run_in_terminal.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/application/run_in_terminal.py @@ -1,117 +1,117 @@ -""" -Tools for running functions on the terminal above the current application or prompt. -""" -import sys -from asyncio import Future, ensure_future -from typing import AsyncGenerator, Awaitable, Callable, TypeVar - -from prompt_toolkit.eventloop import run_in_executor_with_context - -from .current import get_app_or_none - -if sys.version_info >= (3, 7): - from contextlib import asynccontextmanager -else: - from prompt_toolkit.eventloop.async_context_manager import asynccontextmanager - - -__all__ = [ - "run_in_terminal", - "in_terminal", -] - -_T = TypeVar("_T") - - -def run_in_terminal( - func: Callable[[], _T], render_cli_done: bool = False, in_executor: bool = False -) -> Awaitable[_T]: - """ - Run function on the terminal above the current application or prompt. - - What this does is first hiding the prompt, then running this callable - (which can safely output to the terminal), and then again rendering the - prompt which causes the output of this function to scroll above the - prompt. - - ``func`` is supposed to be a synchronous function. If you need an - asynchronous version of this function, use the ``in_terminal`` context - manager directly. - - :param func: The callable to execute. - :param render_cli_done: When True, render the interface in the - 'Done' state first, then execute the function. If False, - erase the interface first. - :param in_executor: When True, run in executor. (Use this for long - blocking functions, when you don't want to block the event loop.) - - :returns: A `Future`. - """ - - async def run() -> _T: - async with in_terminal(render_cli_done=render_cli_done): - if in_executor: - return await run_in_executor_with_context(func) - else: - return func() - - return ensure_future(run()) - - -@asynccontextmanager -async def in_terminal(render_cli_done: bool = False) -> AsyncGenerator[None, None]: - """ - Asynchronous context manager that suspends the current application and runs - the body in the terminal. - - .. code:: - - async def f(): - async with in_terminal(): - call_some_function() - await call_some_async_function() - """ - app = get_app_or_none() - if app is None or not app._is_running: - yield - return - - # When a previous `run_in_terminal` call was in progress. Wait for that - # to finish, before starting this one. Chain to previous call. - previous_run_in_terminal_f = app._running_in_terminal_f - new_run_in_terminal_f: Future[None] = Future() - app._running_in_terminal_f = new_run_in_terminal_f - - # Wait for the previous `run_in_terminal` to finish. - if previous_run_in_terminal_f is not None: - await previous_run_in_terminal_f - - # Wait for all CPRs to arrive. We don't want to detach the input until - # all cursor position responses have been arrived. Otherwise, the tty - # will echo its input and can show stuff like ^[[39;1R. - if app.output.responds_to_cpr: - await app.renderer.wait_for_cpr_responses() - - # Draw interface in 'done' state, or erase. - if render_cli_done: - app._redraw(render_as_done=True) - else: - app.renderer.erase() - - # Disable rendering. - app._running_in_terminal = True - - # Detach input. - try: - with app.input.detach(): - with app.input.cooked_mode(): - yield - finally: - # Redraw interface again. - try: - app._running_in_terminal = False - app.renderer.reset() - app._request_absolute_cursor_position() - app._redraw() - finally: - new_run_in_terminal_f.set_result(None) +""" +Tools for running functions on the terminal above the current application or prompt. +""" +import sys +from asyncio import Future, ensure_future +from typing import AsyncGenerator, Awaitable, Callable, TypeVar + +from prompt_toolkit.eventloop import run_in_executor_with_context + +from .current import get_app_or_none + +if sys.version_info >= (3, 7): + from contextlib import asynccontextmanager +else: + from prompt_toolkit.eventloop.async_context_manager import asynccontextmanager + + +__all__ = [ + "run_in_terminal", + "in_terminal", +] + +_T = TypeVar("_T") + + +def run_in_terminal( + func: Callable[[], _T], render_cli_done: bool = False, in_executor: bool = False +) -> Awaitable[_T]: + """ + Run function on the terminal above the current application or prompt. + + What this does is first hiding the prompt, then running this callable + (which can safely output to the terminal), and then again rendering the + prompt which causes the output of this function to scroll above the + prompt. + + ``func`` is supposed to be a synchronous function. If you need an + asynchronous version of this function, use the ``in_terminal`` context + manager directly. + + :param func: The callable to execute. + :param render_cli_done: When True, render the interface in the + 'Done' state first, then execute the function. If False, + erase the interface first. + :param in_executor: When True, run in executor. (Use this for long + blocking functions, when you don't want to block the event loop.) + + :returns: A `Future`. + """ + + async def run() -> _T: + async with in_terminal(render_cli_done=render_cli_done): + if in_executor: + return await run_in_executor_with_context(func) + else: + return func() + + return ensure_future(run()) + + +@asynccontextmanager +async def in_terminal(render_cli_done: bool = False) -> AsyncGenerator[None, None]: + """ + Asynchronous context manager that suspends the current application and runs + the body in the terminal. + + .. code:: + + async def f(): + async with in_terminal(): + call_some_function() + await call_some_async_function() + """ + app = get_app_or_none() + if app is None or not app._is_running: + yield + return + + # When a previous `run_in_terminal` call was in progress. Wait for that + # to finish, before starting this one. Chain to previous call. + previous_run_in_terminal_f = app._running_in_terminal_f + new_run_in_terminal_f: Future[None] = Future() + app._running_in_terminal_f = new_run_in_terminal_f + + # Wait for the previous `run_in_terminal` to finish. + if previous_run_in_terminal_f is not None: + await previous_run_in_terminal_f + + # Wait for all CPRs to arrive. We don't want to detach the input until + # all cursor position responses have been arrived. Otherwise, the tty + # will echo its input and can show stuff like ^[[39;1R. + if app.output.responds_to_cpr: + await app.renderer.wait_for_cpr_responses() + + # Draw interface in 'done' state, or erase. + if render_cli_done: + app._redraw(render_as_done=True) + else: + app.renderer.erase() + + # Disable rendering. + app._running_in_terminal = True + + # Detach input. + try: + with app.input.detach(): + with app.input.cooked_mode(): + yield + finally: + # Redraw interface again. + try: + app._running_in_terminal = False + app.renderer.reset() + app._request_absolute_cursor_position() + app._redraw() + finally: + new_run_in_terminal_f.set_result(None) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/auto_suggest.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/auto_suggest.py index 84b015d708..7099b8e974 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/auto_suggest.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/auto_suggest.py @@ -1,187 +1,187 @@ -""" -`Fish-style <http://fishshell.com/>`_ like auto-suggestion. - -While a user types input in a certain buffer, suggestions are generated -(asynchronously.) Usually, they are displayed after the input. When the cursor -presses the right arrow and the cursor is at the end of the input, the -suggestion will be inserted. - -If you want the auto suggestions to be asynchronous (in a background thread), -because they take too much time, and could potentially block the event loop, -then wrap the :class:`.AutoSuggest` instance into a -:class:`.ThreadedAutoSuggest`. -""" -from abc import ABCMeta, abstractmethod -from typing import TYPE_CHECKING, Callable, Optional, Union - -from prompt_toolkit.eventloop import run_in_executor_with_context - -from .document import Document -from .filters import Filter, to_filter - -if TYPE_CHECKING: - from .buffer import Buffer - -__all__ = [ - "Suggestion", - "AutoSuggest", - "ThreadedAutoSuggest", - "DummyAutoSuggest", - "AutoSuggestFromHistory", - "ConditionalAutoSuggest", - "DynamicAutoSuggest", -] - - -class Suggestion: - """ - Suggestion returned by an auto-suggest algorithm. - - :param text: The suggestion text. - """ - - def __init__(self, text: str) -> None: - self.text = text - - def __repr__(self) -> str: - return "Suggestion(%s)" % self.text - - -class AutoSuggest(metaclass=ABCMeta): - """ - Base class for auto suggestion implementations. - """ - - @abstractmethod - def get_suggestion( - self, buffer: "Buffer", document: Document - ) -> Optional[Suggestion]: - """ - Return `None` or a :class:`.Suggestion` instance. - - We receive both :class:`~prompt_toolkit.buffer.Buffer` and - :class:`~prompt_toolkit.document.Document`. The reason is that auto - suggestions are retrieved asynchronously. (Like completions.) The - buffer text could be changed in the meantime, but ``document`` contains - the buffer document like it was at the start of the auto suggestion - call. So, from here, don't access ``buffer.text``, but use - ``document.text`` instead. - - :param buffer: The :class:`~prompt_toolkit.buffer.Buffer` instance. - :param document: The :class:`~prompt_toolkit.document.Document` instance. - """ - - async def get_suggestion_async( - self, buff: "Buffer", document: Document - ) -> Optional[Suggestion]: - """ - Return a :class:`.Future` which is set when the suggestions are ready. - This function can be overloaded in order to provide an asynchronous - implementation. - """ - return self.get_suggestion(buff, document) - - -class ThreadedAutoSuggest(AutoSuggest): - """ - Wrapper that runs auto suggestions in a thread. - (Use this to prevent the user interface from becoming unresponsive if the - generation of suggestions takes too much time.) - """ - - def __init__(self, auto_suggest: AutoSuggest) -> None: - self.auto_suggest = auto_suggest - - def get_suggestion( - self, buff: "Buffer", document: Document - ) -> Optional[Suggestion]: - return self.auto_suggest.get_suggestion(buff, document) - - async def get_suggestion_async( - self, buff: "Buffer", document: Document - ) -> Optional[Suggestion]: - """ - Run the `get_suggestion` function in a thread. - """ - - def run_get_suggestion_thread() -> Optional[Suggestion]: - return self.get_suggestion(buff, document) - - return await run_in_executor_with_context(run_get_suggestion_thread) - - -class DummyAutoSuggest(AutoSuggest): - """ - AutoSuggest class that doesn't return any suggestion. - """ - - def get_suggestion( - self, buffer: "Buffer", document: Document - ) -> Optional[Suggestion]: - return None # No suggestion - - -class AutoSuggestFromHistory(AutoSuggest): - """ - Give suggestions based on the lines in the history. - """ - - def get_suggestion( - self, buffer: "Buffer", document: Document - ) -> Optional[Suggestion]: - history = buffer.history - - # Consider only the last line for the suggestion. - text = document.text.rsplit("\n", 1)[-1] - - # Only create a suggestion when this is not an empty line. - if text.strip(): - # Find first matching line in history. - for string in reversed(list(history.get_strings())): - for line in reversed(string.splitlines()): - if line.startswith(text): - return Suggestion(line[len(text) :]) - - return None - - -class ConditionalAutoSuggest(AutoSuggest): - """ - Auto suggest that can be turned on and of according to a certain condition. - """ - - def __init__(self, auto_suggest: AutoSuggest, filter: Union[bool, Filter]) -> None: - - self.auto_suggest = auto_suggest - self.filter = to_filter(filter) - - def get_suggestion( - self, buffer: "Buffer", document: Document - ) -> Optional[Suggestion]: - if self.filter(): - return self.auto_suggest.get_suggestion(buffer, document) - - return None - - -class DynamicAutoSuggest(AutoSuggest): - """ - Validator class that can dynamically returns any Validator. - - :param get_validator: Callable that returns a :class:`.Validator` instance. - """ - - def __init__(self, get_auto_suggest: Callable[[], Optional[AutoSuggest]]) -> None: - self.get_auto_suggest = get_auto_suggest - - def get_suggestion( - self, buff: "Buffer", document: Document - ) -> Optional[Suggestion]: - auto_suggest = self.get_auto_suggest() or DummyAutoSuggest() - return auto_suggest.get_suggestion(buff, document) - - async def get_suggestion_async( - self, buff: "Buffer", document: Document - ) -> Optional[Suggestion]: - auto_suggest = self.get_auto_suggest() or DummyAutoSuggest() - return await auto_suggest.get_suggestion_async(buff, document) +""" +`Fish-style <http://fishshell.com/>`_ like auto-suggestion. + +While a user types input in a certain buffer, suggestions are generated +(asynchronously.) Usually, they are displayed after the input. When the cursor +presses the right arrow and the cursor is at the end of the input, the +suggestion will be inserted. + +If you want the auto suggestions to be asynchronous (in a background thread), +because they take too much time, and could potentially block the event loop, +then wrap the :class:`.AutoSuggest` instance into a +:class:`.ThreadedAutoSuggest`. +""" +from abc import ABCMeta, abstractmethod +from typing import TYPE_CHECKING, Callable, Optional, Union + +from prompt_toolkit.eventloop import run_in_executor_with_context + +from .document import Document +from .filters import Filter, to_filter + +if TYPE_CHECKING: + from .buffer import Buffer + +__all__ = [ + "Suggestion", + "AutoSuggest", + "ThreadedAutoSuggest", + "DummyAutoSuggest", + "AutoSuggestFromHistory", + "ConditionalAutoSuggest", + "DynamicAutoSuggest", +] + + +class Suggestion: + """ + Suggestion returned by an auto-suggest algorithm. + + :param text: The suggestion text. + """ + + def __init__(self, text: str) -> None: + self.text = text + + def __repr__(self) -> str: + return "Suggestion(%s)" % self.text + + +class AutoSuggest(metaclass=ABCMeta): + """ + Base class for auto suggestion implementations. + """ + + @abstractmethod + def get_suggestion( + self, buffer: "Buffer", document: Document + ) -> Optional[Suggestion]: + """ + Return `None` or a :class:`.Suggestion` instance. + + We receive both :class:`~prompt_toolkit.buffer.Buffer` and + :class:`~prompt_toolkit.document.Document`. The reason is that auto + suggestions are retrieved asynchronously. (Like completions.) The + buffer text could be changed in the meantime, but ``document`` contains + the buffer document like it was at the start of the auto suggestion + call. So, from here, don't access ``buffer.text``, but use + ``document.text`` instead. + + :param buffer: The :class:`~prompt_toolkit.buffer.Buffer` instance. + :param document: The :class:`~prompt_toolkit.document.Document` instance. + """ + + async def get_suggestion_async( + self, buff: "Buffer", document: Document + ) -> Optional[Suggestion]: + """ + Return a :class:`.Future` which is set when the suggestions are ready. + This function can be overloaded in order to provide an asynchronous + implementation. + """ + return self.get_suggestion(buff, document) + + +class ThreadedAutoSuggest(AutoSuggest): + """ + Wrapper that runs auto suggestions in a thread. + (Use this to prevent the user interface from becoming unresponsive if the + generation of suggestions takes too much time.) + """ + + def __init__(self, auto_suggest: AutoSuggest) -> None: + self.auto_suggest = auto_suggest + + def get_suggestion( + self, buff: "Buffer", document: Document + ) -> Optional[Suggestion]: + return self.auto_suggest.get_suggestion(buff, document) + + async def get_suggestion_async( + self, buff: "Buffer", document: Document + ) -> Optional[Suggestion]: + """ + Run the `get_suggestion` function in a thread. + """ + + def run_get_suggestion_thread() -> Optional[Suggestion]: + return self.get_suggestion(buff, document) + + return await run_in_executor_with_context(run_get_suggestion_thread) + + +class DummyAutoSuggest(AutoSuggest): + """ + AutoSuggest class that doesn't return any suggestion. + """ + + def get_suggestion( + self, buffer: "Buffer", document: Document + ) -> Optional[Suggestion]: + return None # No suggestion + + +class AutoSuggestFromHistory(AutoSuggest): + """ + Give suggestions based on the lines in the history. + """ + + def get_suggestion( + self, buffer: "Buffer", document: Document + ) -> Optional[Suggestion]: + history = buffer.history + + # Consider only the last line for the suggestion. + text = document.text.rsplit("\n", 1)[-1] + + # Only create a suggestion when this is not an empty line. + if text.strip(): + # Find first matching line in history. + for string in reversed(list(history.get_strings())): + for line in reversed(string.splitlines()): + if line.startswith(text): + return Suggestion(line[len(text) :]) + + return None + + +class ConditionalAutoSuggest(AutoSuggest): + """ + Auto suggest that can be turned on and of according to a certain condition. + """ + + def __init__(self, auto_suggest: AutoSuggest, filter: Union[bool, Filter]) -> None: + + self.auto_suggest = auto_suggest + self.filter = to_filter(filter) + + def get_suggestion( + self, buffer: "Buffer", document: Document + ) -> Optional[Suggestion]: + if self.filter(): + return self.auto_suggest.get_suggestion(buffer, document) + + return None + + +class DynamicAutoSuggest(AutoSuggest): + """ + Validator class that can dynamically returns any Validator. + + :param get_validator: Callable that returns a :class:`.Validator` instance. + """ + + def __init__(self, get_auto_suggest: Callable[[], Optional[AutoSuggest]]) -> None: + self.get_auto_suggest = get_auto_suggest + + def get_suggestion( + self, buff: "Buffer", document: Document + ) -> Optional[Suggestion]: + auto_suggest = self.get_auto_suggest() or DummyAutoSuggest() + return auto_suggest.get_suggestion(buff, document) + + async def get_suggestion_async( + self, buff: "Buffer", document: Document + ) -> Optional[Suggestion]: + auto_suggest = self.get_auto_suggest() or DummyAutoSuggest() + return await auto_suggest.get_suggestion_async(buff, document) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/buffer.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/buffer.py index 67956d8f54..105d3cc07d 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/buffer.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/buffer.py @@ -1,2014 +1,2014 @@ -""" -Data structures for the Buffer. -It holds the text, cursor position, history, etc... -""" -import asyncio -import logging -import os -import re -import shlex -import shutil -import subprocess -import tempfile -from collections import deque -from enum import Enum -from functools import wraps -from typing import ( - Any, - Awaitable, - Callable, - Deque, - Iterable, - List, - Optional, - Set, - Tuple, - TypeVar, - Union, - cast, -) - -from .application.current import get_app -from .application.run_in_terminal import run_in_terminal -from .auto_suggest import AutoSuggest, Suggestion -from .cache import FastDictCache -from .clipboard import ClipboardData -from .completion import ( - CompleteEvent, - Completer, - Completion, - DummyCompleter, - get_common_complete_suffix, -) -from .document import Document -from .filters import FilterOrBool, to_filter -from .history import History, InMemoryHistory -from .search import SearchDirection, SearchState -from .selection import PasteMode, SelectionState, SelectionType -from .utils import Event, to_str -from .validation import ValidationError, Validator - -__all__ = [ - "EditReadOnlyBuffer", - "Buffer", - "CompletionState", - "indent", - "unindent", - "reshape_text", -] - -logger = logging.getLogger(__name__) - - -class EditReadOnlyBuffer(Exception): - "Attempt editing of read-only :class:`.Buffer`." - - -class ValidationState(Enum): - "The validation state of a buffer. This is set after the validation." - VALID = "VALID" - INVALID = "INVALID" - UNKNOWN = "UNKNOWN" - - -class CompletionState: - """ - Immutable class that contains a completion state. - """ - - def __init__( - self, - original_document: "Document", - completions: Optional[List["Completion"]] = None, - complete_index: Optional[int] = None, - ) -> None: - - #: Document as it was when the completion started. - self.original_document = original_document - - #: List of all the current Completion instances which are possible at - #: this point. - self.completions = completions or [] - - #: Position in the `completions` array. - #: This can be `None` to indicate "no completion", the original text. - self.complete_index = complete_index # Position in the `_completions` array. - - def __repr__(self) -> str: - return "%s(%r, <%r> completions, index=%r)" % ( - self.__class__.__name__, - self.original_document, - len(self.completions), - self.complete_index, - ) - - def go_to_index(self, index: Optional[int]) -> None: - """ - Create a new :class:`.CompletionState` object with the new index. - - When `index` is `None` deselect the completion. - """ - if self.completions: - assert index is None or 0 <= index < len(self.completions) - self.complete_index = index - - def new_text_and_position(self) -> Tuple[str, int]: - """ - Return (new_text, new_cursor_position) for this completion. - """ - if self.complete_index is None: - return self.original_document.text, self.original_document.cursor_position - else: - original_text_before_cursor = self.original_document.text_before_cursor - original_text_after_cursor = self.original_document.text_after_cursor - - c = self.completions[self.complete_index] - if c.start_position == 0: - before = original_text_before_cursor - else: - before = original_text_before_cursor[: c.start_position] - - new_text = before + c.text + original_text_after_cursor - new_cursor_position = len(before) + len(c.text) - return new_text, new_cursor_position - - @property - def current_completion(self) -> Optional["Completion"]: - """ - Return the current completion, or return `None` when no completion is - selected. - """ - if self.complete_index is not None: - return self.completions[self.complete_index] - return None - - -_QUOTED_WORDS_RE = re.compile(r"""(\s+|".*?"|'.*?')""") - - -class YankNthArgState: - """ - For yank-last-arg/yank-nth-arg: Keep track of where we are in the history. - """ - - def __init__( - self, history_position: int = 0, n: int = -1, previous_inserted_word: str = "" - ) -> None: - - self.history_position = history_position - self.previous_inserted_word = previous_inserted_word - self.n = n - - def __repr__(self) -> str: - return "%s(history_position=%r, n=%r, previous_inserted_word=%r)" % ( - self.__class__.__name__, - self.history_position, - self.n, - self.previous_inserted_word, - ) - - -BufferEventHandler = Callable[["Buffer"], None] -BufferAcceptHandler = Callable[["Buffer"], bool] - - -class Buffer: - """ - The core data structure that holds the text and cursor position of the - current input line and implements all text manipulations on top of it. It - also implements the history, undo stack and the completion state. - - :param completer: :class:`~prompt_toolkit.completion.Completer` instance. - :param history: :class:`~prompt_toolkit.history.History` instance. - :param tempfile_suffix: The tempfile suffix (extension) to be used for the - "open in editor" function. For a Python REPL, this would be ".py", so - that the editor knows the syntax highlighting to use. This can also be - a callable that returns a string. - :param tempfile: For more advanced tempfile situations where you need - control over the subdirectories and filename. For a Git Commit Message, - this would be ".git/COMMIT_EDITMSG", so that the editor knows the syntax - highlighting to use. This can also be a callable that returns a string. - :param name: Name for this buffer. E.g. DEFAULT_BUFFER. This is mostly - useful for key bindings where we sometimes prefer to refer to a buffer - by their name instead of by reference. - :param accept_handler: Called when the buffer input is accepted. (Usually - when the user presses `enter`.) The accept handler receives this - `Buffer` as input and should return True when the buffer text should be - kept instead of calling reset. - - In case of a `PromptSession` for instance, we want to keep the text, - because we will exit the application, and only reset it during the next - run. - - Events: - - :param on_text_changed: When the buffer text changes. (Callable or None.) - :param on_text_insert: When new text is inserted. (Callable or None.) - :param on_cursor_position_changed: When the cursor moves. (Callable or None.) - :param on_completions_changed: When the completions were changed. (Callable or None.) - :param on_suggestion_set: When an auto-suggestion text has been set. (Callable or None.) - - Filters: - - :param complete_while_typing: :class:`~prompt_toolkit.filters.Filter` - or `bool`. Decide whether or not to do asynchronous autocompleting while - typing. - :param validate_while_typing: :class:`~prompt_toolkit.filters.Filter` - or `bool`. Decide whether or not to do asynchronous validation while - typing. - :param enable_history_search: :class:`~prompt_toolkit.filters.Filter` or - `bool` to indicate when up-arrow partial string matching is enabled. It - is advised to not enable this at the same time as - `complete_while_typing`, because when there is an autocompletion found, - the up arrows usually browse through the completions, rather than - through the history. - :param read_only: :class:`~prompt_toolkit.filters.Filter`. When True, - changes will not be allowed. - :param multiline: :class:`~prompt_toolkit.filters.Filter` or `bool`. When - not set, pressing `Enter` will call the `accept_handler`. Otherwise, - pressing `Esc-Enter` is required. - """ - - def __init__( - self, - completer: Optional[Completer] = None, - auto_suggest: Optional[AutoSuggest] = None, - history: Optional[History] = None, - validator: Optional[Validator] = None, - tempfile_suffix: Union[str, Callable[[], str]] = "", - tempfile: Union[str, Callable[[], str]] = "", - name: str = "", - complete_while_typing: FilterOrBool = False, - validate_while_typing: FilterOrBool = False, - enable_history_search: FilterOrBool = False, - document: Optional[Document] = None, - accept_handler: Optional[BufferAcceptHandler] = None, - read_only: FilterOrBool = False, - multiline: FilterOrBool = True, - on_text_changed: Optional[BufferEventHandler] = None, - on_text_insert: Optional[BufferEventHandler] = None, - on_cursor_position_changed: Optional[BufferEventHandler] = None, - on_completions_changed: Optional[BufferEventHandler] = None, - on_suggestion_set: Optional[BufferEventHandler] = None, - ): - - # Accept both filters and booleans as input. - enable_history_search = to_filter(enable_history_search) - complete_while_typing = to_filter(complete_while_typing) - validate_while_typing = to_filter(validate_while_typing) - read_only = to_filter(read_only) - multiline = to_filter(multiline) - - self.completer = completer or DummyCompleter() - self.auto_suggest = auto_suggest - self.validator = validator - self.tempfile_suffix = tempfile_suffix - self.tempfile = tempfile - self.name = name - self.accept_handler = accept_handler - - # Filters. (Usually, used by the key bindings to drive the buffer.) - self.complete_while_typing = complete_while_typing - self.validate_while_typing = validate_while_typing - self.enable_history_search = enable_history_search - self.read_only = read_only - self.multiline = multiline - - # Text width. (For wrapping, used by the Vi 'gq' operator.) - self.text_width = 0 - - #: The command buffer history. - # Note that we shouldn't use a lazy 'or' here. bool(history) could be - # False when empty. - self.history = InMemoryHistory() if history is None else history - - self.__cursor_position = 0 - - # Events - self.on_text_changed: Event["Buffer"] = Event(self, on_text_changed) - self.on_text_insert: Event["Buffer"] = Event(self, on_text_insert) - self.on_cursor_position_changed: Event["Buffer"] = Event( - self, on_cursor_position_changed - ) - self.on_completions_changed: Event["Buffer"] = Event( - self, on_completions_changed - ) - self.on_suggestion_set: Event["Buffer"] = Event(self, on_suggestion_set) - - # Document cache. (Avoid creating new Document instances.) - self._document_cache: FastDictCache[ - Tuple[str, int, Optional[SelectionState]], Document - ] = FastDictCache(Document, size=10) - - # Create completer / auto suggestion / validation coroutines. - self._async_suggester = self._create_auto_suggest_coroutine() - self._async_completer = self._create_completer_coroutine() - self._async_validator = self._create_auto_validate_coroutine() - - # Asyncio task for populating the history. - self._load_history_task: Optional[asyncio.Future[None]] = None - - # Reset other attributes. - self.reset(document=document) - - def __repr__(self) -> str: - if len(self.text) < 15: - text = self.text - else: - text = self.text[:12] + "..." - - return "<Buffer(name=%r, text=%r) at %r>" % (self.name, text, id(self)) - - def reset( - self, document: Optional[Document] = None, append_to_history: bool = False - ) -> None: - """ - :param append_to_history: Append current input to history first. - """ - if append_to_history: - self.append_to_history() - - document = document or Document() - - self.__cursor_position = document.cursor_position - - # `ValidationError` instance. (Will be set when the input is wrong.) - self.validation_error: Optional[ValidationError] = None - self.validation_state: Optional[ValidationState] = ValidationState.UNKNOWN - - # State of the selection. - self.selection_state: Optional[SelectionState] = None - - # Multiple cursor mode. (When we press 'I' or 'A' in visual-block mode, - # we can insert text on multiple lines at once. This is implemented by - # using multiple cursors.) - self.multiple_cursor_positions: List[int] = [] - - # When doing consecutive up/down movements, prefer to stay at this column. - self.preferred_column: Optional[int] = None - - # State of complete browser - # For interactive completion through Ctrl-N/Ctrl-P. - self.complete_state: Optional[CompletionState] = None - - # State of Emacs yank-nth-arg completion. - self.yank_nth_arg_state: Optional[YankNthArgState] = None # for yank-nth-arg. - - # Remember the document that we had *right before* the last paste - # operation. This is used for rotating through the kill ring. - self.document_before_paste: Optional[Document] = None - - # Current suggestion. - self.suggestion: Optional[Suggestion] = None - - # The history search text. (Used for filtering the history when we - # browse through it.) - self.history_search_text: Optional[str] = None - - # Undo/redo stacks (stack of `(text, cursor_position)`). - self._undo_stack: List[Tuple[str, int]] = [] - self._redo_stack: List[Tuple[str, int]] = [] - - # Cancel history loader. If history loading was still ongoing. - # Cancel the `_load_history_task`, so that next repaint of the - # `BufferControl` we will repopulate it. - if self._load_history_task is not None: - self._load_history_task.cancel() - self._load_history_task = None - - #: The working lines. Similar to history, except that this can be - #: modified. The user can press arrow_up and edit previous entries. - #: Ctrl-C should reset this, and copy the whole history back in here. - #: Enter should process the current command and append to the real - #: history. - self._working_lines: Deque[str] = deque([document.text]) - self.__working_index = 0 - - def load_history_if_not_yet_loaded(self) -> None: - """ - Create task for populating the buffer history (if not yet done). - - Note:: - - This needs to be called from within the event loop of the - application, because history loading is async, and we need to be - sure the right event loop is active. Therefor, we call this method - in the `BufferControl.create_content`. - - There are situations where prompt_toolkit applications are created - in one thread, but will later run in a different thread (Ptpython - is one example. The REPL runs in a separate thread, in order to - prevent interfering with a potential different event loop in the - main thread. The REPL UI however is still created in the main - thread.) We could decide to not support creating prompt_toolkit - objects in one thread and running the application in a different - thread, but history loading is the only place where it matters, and - this solves it. - """ - if self._load_history_task is None: - - async def load_history() -> None: - async for item in self.history.load(): - self._working_lines.appendleft(item) - self.__working_index += 1 - - self._load_history_task = get_app().create_background_task(load_history()) - - def load_history_done(f: "asyncio.Future[None]") -> None: - """ - Handle `load_history` result when either done, cancelled, or - when an exception was raised. - """ - try: - f.result() - except asyncio.CancelledError: - # Ignore cancellation. But handle it, so that we don't get - # this traceback. - pass - except GeneratorExit: - # Probably not needed, but we had situations where - # `GeneratorExit` was raised in `load_history` during - # cancellation. - pass - except BaseException: - # Log error if something goes wrong. (We don't have a - # caller to which we can propagate this exception.) - logger.exception("Loading history failed") - - self._load_history_task.add_done_callback(load_history_done) - - # <getters/setters> - - def _set_text(self, value: str) -> bool: - """set text at current working_index. Return whether it changed.""" - working_index = self.working_index - working_lines = self._working_lines - - original_value = working_lines[working_index] - working_lines[working_index] = value - - # Return True when this text has been changed. - if len(value) != len(original_value): - # For Python 2, it seems that when two strings have a different - # length and one is a prefix of the other, Python still scans - # character by character to see whether the strings are different. - # (Some benchmarking showed significant differences for big - # documents. >100,000 of lines.) - return True - elif value != original_value: - return True - return False - - def _set_cursor_position(self, value: int) -> bool: - """Set cursor position. Return whether it changed.""" - original_position = self.__cursor_position - self.__cursor_position = max(0, value) - - return self.__cursor_position != original_position - - @property - def text(self) -> str: - return self._working_lines[self.working_index] - - @text.setter - def text(self, value: str) -> None: - """ - Setting text. (When doing this, make sure that the cursor_position is - valid for this text. text/cursor_position should be consistent at any time, - otherwise set a Document instead.) - """ - # Ensure cursor position remains within the size of the text. - if self.cursor_position > len(value): - self.cursor_position = len(value) - - # Don't allow editing of read-only buffers. - if self.read_only(): - raise EditReadOnlyBuffer() - - changed = self._set_text(value) - - if changed: - self._text_changed() - - # Reset history search text. - # (Note that this doesn't need to happen when working_index - # changes, which is when we traverse the history. That's why we - # don't do this in `self._text_changed`.) - self.history_search_text = None - - @property - def cursor_position(self) -> int: - return self.__cursor_position - - @cursor_position.setter - def cursor_position(self, value: int) -> None: - """ - Setting cursor position. - """ - assert isinstance(value, int) - - # Ensure cursor position is within the size of the text. - if value > len(self.text): - value = len(self.text) - if value < 0: - value = 0 - - changed = self._set_cursor_position(value) - - if changed: - self._cursor_position_changed() - - @property - def working_index(self) -> int: - return self.__working_index - - @working_index.setter - def working_index(self, value: int) -> None: - if self.__working_index != value: - self.__working_index = value - # Make sure to reset the cursor position, otherwise we end up in - # situations where the cursor position is out of the bounds of the - # text. - self.cursor_position = 0 - self._text_changed() - - def _text_changed(self) -> None: - # Remove any validation errors and complete state. - self.validation_error = None - self.validation_state = ValidationState.UNKNOWN - self.complete_state = None - self.yank_nth_arg_state = None - self.document_before_paste = None - self.selection_state = None - self.suggestion = None - self.preferred_column = None - - # fire 'on_text_changed' event. - self.on_text_changed.fire() - - # Input validation. - # (This happens on all change events, unlike auto completion, also when - # deleting text.) - if self.validator and self.validate_while_typing(): - get_app().create_background_task(self._async_validator()) - - def _cursor_position_changed(self) -> None: - # Remove any complete state. - # (Input validation should only be undone when the cursor position - # changes.) - self.complete_state = None - self.yank_nth_arg_state = None - self.document_before_paste = None - - # Unset preferred_column. (Will be set after the cursor movement, if - # required.) - self.preferred_column = None - - # Note that the cursor position can change if we have a selection the - # new position of the cursor determines the end of the selection. - - # fire 'on_cursor_position_changed' event. - self.on_cursor_position_changed.fire() - - @property - def document(self) -> Document: - """ - Return :class:`~prompt_toolkit.document.Document` instance from the - current text, cursor position and selection state. - """ - return self._document_cache[ - self.text, self.cursor_position, self.selection_state - ] - - @document.setter - def document(self, value: Document) -> None: - """ - Set :class:`~prompt_toolkit.document.Document` instance. - - This will set both the text and cursor position at the same time, but - atomically. (Change events will be triggered only after both have been set.) - """ - self.set_document(value) - - def set_document(self, value: Document, bypass_readonly: bool = False) -> None: - """ - Set :class:`~prompt_toolkit.document.Document` instance. Like the - ``document`` property, but accept an ``bypass_readonly`` argument. - - :param bypass_readonly: When True, don't raise an - :class:`.EditReadOnlyBuffer` exception, even - when the buffer is read-only. - - .. warning:: - - When this buffer is read-only and `bypass_readonly` was not passed, - the `EditReadOnlyBuffer` exception will be caught by the - `KeyProcessor` and is silently suppressed. This is important to - keep in mind when writing key bindings, because it won't do what - you expect, and there won't be a stack trace. Use try/finally - around this function if you need some cleanup code. - """ - # Don't allow editing of read-only buffers. - if not bypass_readonly and self.read_only(): - raise EditReadOnlyBuffer() - - # Set text and cursor position first. - text_changed = self._set_text(value.text) - cursor_position_changed = self._set_cursor_position(value.cursor_position) - - # Now handle change events. (We do this when text/cursor position is - # both set and consistent.) - if text_changed: - self._text_changed() - self.history_search_text = None - - if cursor_position_changed: - self._cursor_position_changed() - - @property - def is_returnable(self) -> bool: - """ - True when there is something handling accept. - """ - return bool(self.accept_handler) - - # End of <getters/setters> - - def save_to_undo_stack(self, clear_redo_stack: bool = True) -> None: - """ - Safe current state (input text and cursor position), so that we can - restore it by calling undo. - """ - # Safe if the text is different from the text at the top of the stack - # is different. If the text is the same, just update the cursor position. - if self._undo_stack and self._undo_stack[-1][0] == self.text: - self._undo_stack[-1] = (self._undo_stack[-1][0], self.cursor_position) - else: - self._undo_stack.append((self.text, self.cursor_position)) - - # Saving anything to the undo stack, clears the redo stack. - if clear_redo_stack: - self._redo_stack = [] - - def transform_lines( - self, - line_index_iterator: Iterable[int], - transform_callback: Callable[[str], str], - ) -> str: - """ - Transforms the text on a range of lines. - When the iterator yield an index not in the range of lines that the - document contains, it skips them silently. - - To uppercase some lines:: - - new_text = transform_lines(range(5,10), lambda text: text.upper()) - - :param line_index_iterator: Iterator of line numbers (int) - :param transform_callback: callable that takes the original text of a - line, and return the new text for this line. - - :returns: The new text. - """ - # Split lines - lines = self.text.split("\n") - - # Apply transformation - for index in line_index_iterator: - try: - lines[index] = transform_callback(lines[index]) - except IndexError: - pass - - return "\n".join(lines) - - def transform_current_line(self, transform_callback: Callable[[str], str]) -> None: - """ - Apply the given transformation function to the current line. - - :param transform_callback: callable that takes a string and return a new string. - """ - document = self.document - a = document.cursor_position + document.get_start_of_line_position() - b = document.cursor_position + document.get_end_of_line_position() - self.text = ( - document.text[:a] - + transform_callback(document.text[a:b]) - + document.text[b:] - ) - - def transform_region( - self, from_: int, to: int, transform_callback: Callable[[str], str] - ) -> None: - """ - Transform a part of the input string. - - :param from_: (int) start position. - :param to: (int) end position. - :param transform_callback: Callable which accepts a string and returns - the transformed string. - """ - assert from_ < to - - self.text = "".join( - [ - self.text[:from_] - + transform_callback(self.text[from_:to]) - + self.text[to:] - ] - ) - - def cursor_left(self, count: int = 1) -> None: - self.cursor_position += self.document.get_cursor_left_position(count=count) - - def cursor_right(self, count: int = 1) -> None: - self.cursor_position += self.document.get_cursor_right_position(count=count) - - def cursor_up(self, count: int = 1) -> None: - """(for multiline edit). Move cursor to the previous line.""" - original_column = self.preferred_column or self.document.cursor_position_col - self.cursor_position += self.document.get_cursor_up_position( - count=count, preferred_column=original_column - ) - - # Remember the original column for the next up/down movement. - self.preferred_column = original_column - - def cursor_down(self, count: int = 1) -> None: - """(for multiline edit). Move cursor to the next line.""" - original_column = self.preferred_column or self.document.cursor_position_col - self.cursor_position += self.document.get_cursor_down_position( - count=count, preferred_column=original_column - ) - - # Remember the original column for the next up/down movement. - self.preferred_column = original_column - - def auto_up( - self, count: int = 1, go_to_start_of_line_if_history_changes: bool = False - ) -> None: - """ - If we're not on the first line (of a multiline input) go a line up, - otherwise go back in history. (If nothing is selected.) - """ - if self.complete_state: - self.complete_previous(count=count) - elif self.document.cursor_position_row > 0: - self.cursor_up(count=count) - elif not self.selection_state: - self.history_backward(count=count) - - # Go to the start of the line? - if go_to_start_of_line_if_history_changes: - self.cursor_position += self.document.get_start_of_line_position() - - def auto_down( - self, count: int = 1, go_to_start_of_line_if_history_changes: bool = False - ) -> None: - """ - If we're not on the last line (of a multiline input) go a line down, - otherwise go forward in history. (If nothing is selected.) - """ - if self.complete_state: - self.complete_next(count=count) - elif self.document.cursor_position_row < self.document.line_count - 1: - self.cursor_down(count=count) - elif not self.selection_state: - self.history_forward(count=count) - - # Go to the start of the line? - if go_to_start_of_line_if_history_changes: - self.cursor_position += self.document.get_start_of_line_position() - - def delete_before_cursor(self, count: int = 1) -> str: - """ - Delete specified number of characters before cursor and return the - deleted text. - """ - assert count >= 0 - deleted = "" - - if self.cursor_position > 0: - deleted = self.text[self.cursor_position - count : self.cursor_position] - - new_text = ( - self.text[: self.cursor_position - count] - + self.text[self.cursor_position :] - ) - new_cursor_position = self.cursor_position - len(deleted) - - # Set new Document atomically. - self.document = Document(new_text, new_cursor_position) - - return deleted - - def delete(self, count: int = 1) -> str: - """ - Delete specified number of characters and Return the deleted text. - """ - if self.cursor_position < len(self.text): - deleted = self.document.text_after_cursor[:count] - self.text = ( - self.text[: self.cursor_position] - + self.text[self.cursor_position + len(deleted) :] - ) - return deleted - else: - return "" - - def join_next_line(self, separator: str = " ") -> None: - """ - Join the next line to the current one by deleting the line ending after - the current line. - """ - if not self.document.on_last_line: - self.cursor_position += self.document.get_end_of_line_position() - self.delete() - - # Remove spaces. - self.text = ( - self.document.text_before_cursor - + separator - + self.document.text_after_cursor.lstrip(" ") - ) - - def join_selected_lines(self, separator: str = " ") -> None: - """ - Join the selected lines. - """ - assert self.selection_state - - # Get lines. - from_, to = sorted( - [self.cursor_position, self.selection_state.original_cursor_position] - ) - - before = self.text[:from_] - lines = self.text[from_:to].splitlines() - after = self.text[to:] - - # Replace leading spaces with just one space. - lines = [l.lstrip(" ") + separator for l in lines] - - # Set new document. - self.document = Document( - text=before + "".join(lines) + after, - cursor_position=len(before + "".join(lines[:-1])) - 1, - ) - - def swap_characters_before_cursor(self) -> None: - """ - Swap the last two characters before the cursor. - """ - pos = self.cursor_position - - if pos >= 2: - a = self.text[pos - 2] - b = self.text[pos - 1] - - self.text = self.text[: pos - 2] + b + a + self.text[pos:] - - def go_to_history(self, index: int) -> None: - """ - Go to this item in the history. - """ - if index < len(self._working_lines): - self.working_index = index - self.cursor_position = len(self.text) - - def complete_next(self, count: int = 1, disable_wrap_around: bool = False) -> None: - """ - Browse to the next completions. - (Does nothing if there are no completion.) - """ - index: Optional[int] - - if self.complete_state: - completions_count = len(self.complete_state.completions) - - if self.complete_state.complete_index is None: - index = 0 - elif self.complete_state.complete_index == completions_count - 1: - index = None - - if disable_wrap_around: - return - else: - index = min( - completions_count - 1, self.complete_state.complete_index + count - ) - self.go_to_completion(index) - - def complete_previous( - self, count: int = 1, disable_wrap_around: bool = False - ) -> None: - """ - Browse to the previous completions. - (Does nothing if there are no completion.) - """ - index: Optional[int] - - if self.complete_state: - if self.complete_state.complete_index == 0: - index = None - - if disable_wrap_around: - return - elif self.complete_state.complete_index is None: - index = len(self.complete_state.completions) - 1 - else: - index = max(0, self.complete_state.complete_index - count) - - self.go_to_completion(index) - - def cancel_completion(self) -> None: - """ - Cancel completion, go back to the original text. - """ - if self.complete_state: - self.go_to_completion(None) - self.complete_state = None - - def _set_completions(self, completions: List[Completion]) -> CompletionState: - """ - Start completions. (Generate list of completions and initialize.) - - By default, no completion will be selected. - """ - self.complete_state = CompletionState( - original_document=self.document, completions=completions - ) - - # Trigger event. This should eventually invalidate the layout. - self.on_completions_changed.fire() - - return self.complete_state - - def start_history_lines_completion(self) -> None: - """ - Start a completion based on all the other lines in the document and the - history. - """ - found_completions: Set[str] = set() - completions = [] - - # For every line of the whole history, find matches with the current line. - current_line = self.document.current_line_before_cursor.lstrip() - - for i, string in enumerate(self._working_lines): - for j, l in enumerate(string.split("\n")): - l = l.strip() - if l and l.startswith(current_line): - # When a new line has been found. - if l not in found_completions: - found_completions.add(l) - - # Create completion. - if i == self.working_index: - display_meta = "Current, line %s" % (j + 1) - else: - display_meta = "History %s, line %s" % (i + 1, j + 1) - - completions.append( - Completion( +""" +Data structures for the Buffer. +It holds the text, cursor position, history, etc... +""" +import asyncio +import logging +import os +import re +import shlex +import shutil +import subprocess +import tempfile +from collections import deque +from enum import Enum +from functools import wraps +from typing import ( + Any, + Awaitable, + Callable, + Deque, + Iterable, + List, + Optional, + Set, + Tuple, + TypeVar, + Union, + cast, +) + +from .application.current import get_app +from .application.run_in_terminal import run_in_terminal +from .auto_suggest import AutoSuggest, Suggestion +from .cache import FastDictCache +from .clipboard import ClipboardData +from .completion import ( + CompleteEvent, + Completer, + Completion, + DummyCompleter, + get_common_complete_suffix, +) +from .document import Document +from .filters import FilterOrBool, to_filter +from .history import History, InMemoryHistory +from .search import SearchDirection, SearchState +from .selection import PasteMode, SelectionState, SelectionType +from .utils import Event, to_str +from .validation import ValidationError, Validator + +__all__ = [ + "EditReadOnlyBuffer", + "Buffer", + "CompletionState", + "indent", + "unindent", + "reshape_text", +] + +logger = logging.getLogger(__name__) + + +class EditReadOnlyBuffer(Exception): + "Attempt editing of read-only :class:`.Buffer`." + + +class ValidationState(Enum): + "The validation state of a buffer. This is set after the validation." + VALID = "VALID" + INVALID = "INVALID" + UNKNOWN = "UNKNOWN" + + +class CompletionState: + """ + Immutable class that contains a completion state. + """ + + def __init__( + self, + original_document: "Document", + completions: Optional[List["Completion"]] = None, + complete_index: Optional[int] = None, + ) -> None: + + #: Document as it was when the completion started. + self.original_document = original_document + + #: List of all the current Completion instances which are possible at + #: this point. + self.completions = completions or [] + + #: Position in the `completions` array. + #: This can be `None` to indicate "no completion", the original text. + self.complete_index = complete_index # Position in the `_completions` array. + + def __repr__(self) -> str: + return "%s(%r, <%r> completions, index=%r)" % ( + self.__class__.__name__, + self.original_document, + len(self.completions), + self.complete_index, + ) + + def go_to_index(self, index: Optional[int]) -> None: + """ + Create a new :class:`.CompletionState` object with the new index. + + When `index` is `None` deselect the completion. + """ + if self.completions: + assert index is None or 0 <= index < len(self.completions) + self.complete_index = index + + def new_text_and_position(self) -> Tuple[str, int]: + """ + Return (new_text, new_cursor_position) for this completion. + """ + if self.complete_index is None: + return self.original_document.text, self.original_document.cursor_position + else: + original_text_before_cursor = self.original_document.text_before_cursor + original_text_after_cursor = self.original_document.text_after_cursor + + c = self.completions[self.complete_index] + if c.start_position == 0: + before = original_text_before_cursor + else: + before = original_text_before_cursor[: c.start_position] + + new_text = before + c.text + original_text_after_cursor + new_cursor_position = len(before) + len(c.text) + return new_text, new_cursor_position + + @property + def current_completion(self) -> Optional["Completion"]: + """ + Return the current completion, or return `None` when no completion is + selected. + """ + if self.complete_index is not None: + return self.completions[self.complete_index] + return None + + +_QUOTED_WORDS_RE = re.compile(r"""(\s+|".*?"|'.*?')""") + + +class YankNthArgState: + """ + For yank-last-arg/yank-nth-arg: Keep track of where we are in the history. + """ + + def __init__( + self, history_position: int = 0, n: int = -1, previous_inserted_word: str = "" + ) -> None: + + self.history_position = history_position + self.previous_inserted_word = previous_inserted_word + self.n = n + + def __repr__(self) -> str: + return "%s(history_position=%r, n=%r, previous_inserted_word=%r)" % ( + self.__class__.__name__, + self.history_position, + self.n, + self.previous_inserted_word, + ) + + +BufferEventHandler = Callable[["Buffer"], None] +BufferAcceptHandler = Callable[["Buffer"], bool] + + +class Buffer: + """ + The core data structure that holds the text and cursor position of the + current input line and implements all text manipulations on top of it. It + also implements the history, undo stack and the completion state. + + :param completer: :class:`~prompt_toolkit.completion.Completer` instance. + :param history: :class:`~prompt_toolkit.history.History` instance. + :param tempfile_suffix: The tempfile suffix (extension) to be used for the + "open in editor" function. For a Python REPL, this would be ".py", so + that the editor knows the syntax highlighting to use. This can also be + a callable that returns a string. + :param tempfile: For more advanced tempfile situations where you need + control over the subdirectories and filename. For a Git Commit Message, + this would be ".git/COMMIT_EDITMSG", so that the editor knows the syntax + highlighting to use. This can also be a callable that returns a string. + :param name: Name for this buffer. E.g. DEFAULT_BUFFER. This is mostly + useful for key bindings where we sometimes prefer to refer to a buffer + by their name instead of by reference. + :param accept_handler: Called when the buffer input is accepted. (Usually + when the user presses `enter`.) The accept handler receives this + `Buffer` as input and should return True when the buffer text should be + kept instead of calling reset. + + In case of a `PromptSession` for instance, we want to keep the text, + because we will exit the application, and only reset it during the next + run. + + Events: + + :param on_text_changed: When the buffer text changes. (Callable or None.) + :param on_text_insert: When new text is inserted. (Callable or None.) + :param on_cursor_position_changed: When the cursor moves. (Callable or None.) + :param on_completions_changed: When the completions were changed. (Callable or None.) + :param on_suggestion_set: When an auto-suggestion text has been set. (Callable or None.) + + Filters: + + :param complete_while_typing: :class:`~prompt_toolkit.filters.Filter` + or `bool`. Decide whether or not to do asynchronous autocompleting while + typing. + :param validate_while_typing: :class:`~prompt_toolkit.filters.Filter` + or `bool`. Decide whether or not to do asynchronous validation while + typing. + :param enable_history_search: :class:`~prompt_toolkit.filters.Filter` or + `bool` to indicate when up-arrow partial string matching is enabled. It + is advised to not enable this at the same time as + `complete_while_typing`, because when there is an autocompletion found, + the up arrows usually browse through the completions, rather than + through the history. + :param read_only: :class:`~prompt_toolkit.filters.Filter`. When True, + changes will not be allowed. + :param multiline: :class:`~prompt_toolkit.filters.Filter` or `bool`. When + not set, pressing `Enter` will call the `accept_handler`. Otherwise, + pressing `Esc-Enter` is required. + """ + + def __init__( + self, + completer: Optional[Completer] = None, + auto_suggest: Optional[AutoSuggest] = None, + history: Optional[History] = None, + validator: Optional[Validator] = None, + tempfile_suffix: Union[str, Callable[[], str]] = "", + tempfile: Union[str, Callable[[], str]] = "", + name: str = "", + complete_while_typing: FilterOrBool = False, + validate_while_typing: FilterOrBool = False, + enable_history_search: FilterOrBool = False, + document: Optional[Document] = None, + accept_handler: Optional[BufferAcceptHandler] = None, + read_only: FilterOrBool = False, + multiline: FilterOrBool = True, + on_text_changed: Optional[BufferEventHandler] = None, + on_text_insert: Optional[BufferEventHandler] = None, + on_cursor_position_changed: Optional[BufferEventHandler] = None, + on_completions_changed: Optional[BufferEventHandler] = None, + on_suggestion_set: Optional[BufferEventHandler] = None, + ): + + # Accept both filters and booleans as input. + enable_history_search = to_filter(enable_history_search) + complete_while_typing = to_filter(complete_while_typing) + validate_while_typing = to_filter(validate_while_typing) + read_only = to_filter(read_only) + multiline = to_filter(multiline) + + self.completer = completer or DummyCompleter() + self.auto_suggest = auto_suggest + self.validator = validator + self.tempfile_suffix = tempfile_suffix + self.tempfile = tempfile + self.name = name + self.accept_handler = accept_handler + + # Filters. (Usually, used by the key bindings to drive the buffer.) + self.complete_while_typing = complete_while_typing + self.validate_while_typing = validate_while_typing + self.enable_history_search = enable_history_search + self.read_only = read_only + self.multiline = multiline + + # Text width. (For wrapping, used by the Vi 'gq' operator.) + self.text_width = 0 + + #: The command buffer history. + # Note that we shouldn't use a lazy 'or' here. bool(history) could be + # False when empty. + self.history = InMemoryHistory() if history is None else history + + self.__cursor_position = 0 + + # Events + self.on_text_changed: Event["Buffer"] = Event(self, on_text_changed) + self.on_text_insert: Event["Buffer"] = Event(self, on_text_insert) + self.on_cursor_position_changed: Event["Buffer"] = Event( + self, on_cursor_position_changed + ) + self.on_completions_changed: Event["Buffer"] = Event( + self, on_completions_changed + ) + self.on_suggestion_set: Event["Buffer"] = Event(self, on_suggestion_set) + + # Document cache. (Avoid creating new Document instances.) + self._document_cache: FastDictCache[ + Tuple[str, int, Optional[SelectionState]], Document + ] = FastDictCache(Document, size=10) + + # Create completer / auto suggestion / validation coroutines. + self._async_suggester = self._create_auto_suggest_coroutine() + self._async_completer = self._create_completer_coroutine() + self._async_validator = self._create_auto_validate_coroutine() + + # Asyncio task for populating the history. + self._load_history_task: Optional[asyncio.Future[None]] = None + + # Reset other attributes. + self.reset(document=document) + + def __repr__(self) -> str: + if len(self.text) < 15: + text = self.text + else: + text = self.text[:12] + "..." + + return "<Buffer(name=%r, text=%r) at %r>" % (self.name, text, id(self)) + + def reset( + self, document: Optional[Document] = None, append_to_history: bool = False + ) -> None: + """ + :param append_to_history: Append current input to history first. + """ + if append_to_history: + self.append_to_history() + + document = document or Document() + + self.__cursor_position = document.cursor_position + + # `ValidationError` instance. (Will be set when the input is wrong.) + self.validation_error: Optional[ValidationError] = None + self.validation_state: Optional[ValidationState] = ValidationState.UNKNOWN + + # State of the selection. + self.selection_state: Optional[SelectionState] = None + + # Multiple cursor mode. (When we press 'I' or 'A' in visual-block mode, + # we can insert text on multiple lines at once. This is implemented by + # using multiple cursors.) + self.multiple_cursor_positions: List[int] = [] + + # When doing consecutive up/down movements, prefer to stay at this column. + self.preferred_column: Optional[int] = None + + # State of complete browser + # For interactive completion through Ctrl-N/Ctrl-P. + self.complete_state: Optional[CompletionState] = None + + # State of Emacs yank-nth-arg completion. + self.yank_nth_arg_state: Optional[YankNthArgState] = None # for yank-nth-arg. + + # Remember the document that we had *right before* the last paste + # operation. This is used for rotating through the kill ring. + self.document_before_paste: Optional[Document] = None + + # Current suggestion. + self.suggestion: Optional[Suggestion] = None + + # The history search text. (Used for filtering the history when we + # browse through it.) + self.history_search_text: Optional[str] = None + + # Undo/redo stacks (stack of `(text, cursor_position)`). + self._undo_stack: List[Tuple[str, int]] = [] + self._redo_stack: List[Tuple[str, int]] = [] + + # Cancel history loader. If history loading was still ongoing. + # Cancel the `_load_history_task`, so that next repaint of the + # `BufferControl` we will repopulate it. + if self._load_history_task is not None: + self._load_history_task.cancel() + self._load_history_task = None + + #: The working lines. Similar to history, except that this can be + #: modified. The user can press arrow_up and edit previous entries. + #: Ctrl-C should reset this, and copy the whole history back in here. + #: Enter should process the current command and append to the real + #: history. + self._working_lines: Deque[str] = deque([document.text]) + self.__working_index = 0 + + def load_history_if_not_yet_loaded(self) -> None: + """ + Create task for populating the buffer history (if not yet done). + + Note:: + + This needs to be called from within the event loop of the + application, because history loading is async, and we need to be + sure the right event loop is active. Therefor, we call this method + in the `BufferControl.create_content`. + + There are situations where prompt_toolkit applications are created + in one thread, but will later run in a different thread (Ptpython + is one example. The REPL runs in a separate thread, in order to + prevent interfering with a potential different event loop in the + main thread. The REPL UI however is still created in the main + thread.) We could decide to not support creating prompt_toolkit + objects in one thread and running the application in a different + thread, but history loading is the only place where it matters, and + this solves it. + """ + if self._load_history_task is None: + + async def load_history() -> None: + async for item in self.history.load(): + self._working_lines.appendleft(item) + self.__working_index += 1 + + self._load_history_task = get_app().create_background_task(load_history()) + + def load_history_done(f: "asyncio.Future[None]") -> None: + """ + Handle `load_history` result when either done, cancelled, or + when an exception was raised. + """ + try: + f.result() + except asyncio.CancelledError: + # Ignore cancellation. But handle it, so that we don't get + # this traceback. + pass + except GeneratorExit: + # Probably not needed, but we had situations where + # `GeneratorExit` was raised in `load_history` during + # cancellation. + pass + except BaseException: + # Log error if something goes wrong. (We don't have a + # caller to which we can propagate this exception.) + logger.exception("Loading history failed") + + self._load_history_task.add_done_callback(load_history_done) + + # <getters/setters> + + def _set_text(self, value: str) -> bool: + """set text at current working_index. Return whether it changed.""" + working_index = self.working_index + working_lines = self._working_lines + + original_value = working_lines[working_index] + working_lines[working_index] = value + + # Return True when this text has been changed. + if len(value) != len(original_value): + # For Python 2, it seems that when two strings have a different + # length and one is a prefix of the other, Python still scans + # character by character to see whether the strings are different. + # (Some benchmarking showed significant differences for big + # documents. >100,000 of lines.) + return True + elif value != original_value: + return True + return False + + def _set_cursor_position(self, value: int) -> bool: + """Set cursor position. Return whether it changed.""" + original_position = self.__cursor_position + self.__cursor_position = max(0, value) + + return self.__cursor_position != original_position + + @property + def text(self) -> str: + return self._working_lines[self.working_index] + + @text.setter + def text(self, value: str) -> None: + """ + Setting text. (When doing this, make sure that the cursor_position is + valid for this text. text/cursor_position should be consistent at any time, + otherwise set a Document instead.) + """ + # Ensure cursor position remains within the size of the text. + if self.cursor_position > len(value): + self.cursor_position = len(value) + + # Don't allow editing of read-only buffers. + if self.read_only(): + raise EditReadOnlyBuffer() + + changed = self._set_text(value) + + if changed: + self._text_changed() + + # Reset history search text. + # (Note that this doesn't need to happen when working_index + # changes, which is when we traverse the history. That's why we + # don't do this in `self._text_changed`.) + self.history_search_text = None + + @property + def cursor_position(self) -> int: + return self.__cursor_position + + @cursor_position.setter + def cursor_position(self, value: int) -> None: + """ + Setting cursor position. + """ + assert isinstance(value, int) + + # Ensure cursor position is within the size of the text. + if value > len(self.text): + value = len(self.text) + if value < 0: + value = 0 + + changed = self._set_cursor_position(value) + + if changed: + self._cursor_position_changed() + + @property + def working_index(self) -> int: + return self.__working_index + + @working_index.setter + def working_index(self, value: int) -> None: + if self.__working_index != value: + self.__working_index = value + # Make sure to reset the cursor position, otherwise we end up in + # situations where the cursor position is out of the bounds of the + # text. + self.cursor_position = 0 + self._text_changed() + + def _text_changed(self) -> None: + # Remove any validation errors and complete state. + self.validation_error = None + self.validation_state = ValidationState.UNKNOWN + self.complete_state = None + self.yank_nth_arg_state = None + self.document_before_paste = None + self.selection_state = None + self.suggestion = None + self.preferred_column = None + + # fire 'on_text_changed' event. + self.on_text_changed.fire() + + # Input validation. + # (This happens on all change events, unlike auto completion, also when + # deleting text.) + if self.validator and self.validate_while_typing(): + get_app().create_background_task(self._async_validator()) + + def _cursor_position_changed(self) -> None: + # Remove any complete state. + # (Input validation should only be undone when the cursor position + # changes.) + self.complete_state = None + self.yank_nth_arg_state = None + self.document_before_paste = None + + # Unset preferred_column. (Will be set after the cursor movement, if + # required.) + self.preferred_column = None + + # Note that the cursor position can change if we have a selection the + # new position of the cursor determines the end of the selection. + + # fire 'on_cursor_position_changed' event. + self.on_cursor_position_changed.fire() + + @property + def document(self) -> Document: + """ + Return :class:`~prompt_toolkit.document.Document` instance from the + current text, cursor position and selection state. + """ + return self._document_cache[ + self.text, self.cursor_position, self.selection_state + ] + + @document.setter + def document(self, value: Document) -> None: + """ + Set :class:`~prompt_toolkit.document.Document` instance. + + This will set both the text and cursor position at the same time, but + atomically. (Change events will be triggered only after both have been set.) + """ + self.set_document(value) + + def set_document(self, value: Document, bypass_readonly: bool = False) -> None: + """ + Set :class:`~prompt_toolkit.document.Document` instance. Like the + ``document`` property, but accept an ``bypass_readonly`` argument. + + :param bypass_readonly: When True, don't raise an + :class:`.EditReadOnlyBuffer` exception, even + when the buffer is read-only. + + .. warning:: + + When this buffer is read-only and `bypass_readonly` was not passed, + the `EditReadOnlyBuffer` exception will be caught by the + `KeyProcessor` and is silently suppressed. This is important to + keep in mind when writing key bindings, because it won't do what + you expect, and there won't be a stack trace. Use try/finally + around this function if you need some cleanup code. + """ + # Don't allow editing of read-only buffers. + if not bypass_readonly and self.read_only(): + raise EditReadOnlyBuffer() + + # Set text and cursor position first. + text_changed = self._set_text(value.text) + cursor_position_changed = self._set_cursor_position(value.cursor_position) + + # Now handle change events. (We do this when text/cursor position is + # both set and consistent.) + if text_changed: + self._text_changed() + self.history_search_text = None + + if cursor_position_changed: + self._cursor_position_changed() + + @property + def is_returnable(self) -> bool: + """ + True when there is something handling accept. + """ + return bool(self.accept_handler) + + # End of <getters/setters> + + def save_to_undo_stack(self, clear_redo_stack: bool = True) -> None: + """ + Safe current state (input text and cursor position), so that we can + restore it by calling undo. + """ + # Safe if the text is different from the text at the top of the stack + # is different. If the text is the same, just update the cursor position. + if self._undo_stack and self._undo_stack[-1][0] == self.text: + self._undo_stack[-1] = (self._undo_stack[-1][0], self.cursor_position) + else: + self._undo_stack.append((self.text, self.cursor_position)) + + # Saving anything to the undo stack, clears the redo stack. + if clear_redo_stack: + self._redo_stack = [] + + def transform_lines( + self, + line_index_iterator: Iterable[int], + transform_callback: Callable[[str], str], + ) -> str: + """ + Transforms the text on a range of lines. + When the iterator yield an index not in the range of lines that the + document contains, it skips them silently. + + To uppercase some lines:: + + new_text = transform_lines(range(5,10), lambda text: text.upper()) + + :param line_index_iterator: Iterator of line numbers (int) + :param transform_callback: callable that takes the original text of a + line, and return the new text for this line. + + :returns: The new text. + """ + # Split lines + lines = self.text.split("\n") + + # Apply transformation + for index in line_index_iterator: + try: + lines[index] = transform_callback(lines[index]) + except IndexError: + pass + + return "\n".join(lines) + + def transform_current_line(self, transform_callback: Callable[[str], str]) -> None: + """ + Apply the given transformation function to the current line. + + :param transform_callback: callable that takes a string and return a new string. + """ + document = self.document + a = document.cursor_position + document.get_start_of_line_position() + b = document.cursor_position + document.get_end_of_line_position() + self.text = ( + document.text[:a] + + transform_callback(document.text[a:b]) + + document.text[b:] + ) + + def transform_region( + self, from_: int, to: int, transform_callback: Callable[[str], str] + ) -> None: + """ + Transform a part of the input string. + + :param from_: (int) start position. + :param to: (int) end position. + :param transform_callback: Callable which accepts a string and returns + the transformed string. + """ + assert from_ < to + + self.text = "".join( + [ + self.text[:from_] + + transform_callback(self.text[from_:to]) + + self.text[to:] + ] + ) + + def cursor_left(self, count: int = 1) -> None: + self.cursor_position += self.document.get_cursor_left_position(count=count) + + def cursor_right(self, count: int = 1) -> None: + self.cursor_position += self.document.get_cursor_right_position(count=count) + + def cursor_up(self, count: int = 1) -> None: + """(for multiline edit). Move cursor to the previous line.""" + original_column = self.preferred_column or self.document.cursor_position_col + self.cursor_position += self.document.get_cursor_up_position( + count=count, preferred_column=original_column + ) + + # Remember the original column for the next up/down movement. + self.preferred_column = original_column + + def cursor_down(self, count: int = 1) -> None: + """(for multiline edit). Move cursor to the next line.""" + original_column = self.preferred_column or self.document.cursor_position_col + self.cursor_position += self.document.get_cursor_down_position( + count=count, preferred_column=original_column + ) + + # Remember the original column for the next up/down movement. + self.preferred_column = original_column + + def auto_up( + self, count: int = 1, go_to_start_of_line_if_history_changes: bool = False + ) -> None: + """ + If we're not on the first line (of a multiline input) go a line up, + otherwise go back in history. (If nothing is selected.) + """ + if self.complete_state: + self.complete_previous(count=count) + elif self.document.cursor_position_row > 0: + self.cursor_up(count=count) + elif not self.selection_state: + self.history_backward(count=count) + + # Go to the start of the line? + if go_to_start_of_line_if_history_changes: + self.cursor_position += self.document.get_start_of_line_position() + + def auto_down( + self, count: int = 1, go_to_start_of_line_if_history_changes: bool = False + ) -> None: + """ + If we're not on the last line (of a multiline input) go a line down, + otherwise go forward in history. (If nothing is selected.) + """ + if self.complete_state: + self.complete_next(count=count) + elif self.document.cursor_position_row < self.document.line_count - 1: + self.cursor_down(count=count) + elif not self.selection_state: + self.history_forward(count=count) + + # Go to the start of the line? + if go_to_start_of_line_if_history_changes: + self.cursor_position += self.document.get_start_of_line_position() + + def delete_before_cursor(self, count: int = 1) -> str: + """ + Delete specified number of characters before cursor and return the + deleted text. + """ + assert count >= 0 + deleted = "" + + if self.cursor_position > 0: + deleted = self.text[self.cursor_position - count : self.cursor_position] + + new_text = ( + self.text[: self.cursor_position - count] + + self.text[self.cursor_position :] + ) + new_cursor_position = self.cursor_position - len(deleted) + + # Set new Document atomically. + self.document = Document(new_text, new_cursor_position) + + return deleted + + def delete(self, count: int = 1) -> str: + """ + Delete specified number of characters and Return the deleted text. + """ + if self.cursor_position < len(self.text): + deleted = self.document.text_after_cursor[:count] + self.text = ( + self.text[: self.cursor_position] + + self.text[self.cursor_position + len(deleted) :] + ) + return deleted + else: + return "" + + def join_next_line(self, separator: str = " ") -> None: + """ + Join the next line to the current one by deleting the line ending after + the current line. + """ + if not self.document.on_last_line: + self.cursor_position += self.document.get_end_of_line_position() + self.delete() + + # Remove spaces. + self.text = ( + self.document.text_before_cursor + + separator + + self.document.text_after_cursor.lstrip(" ") + ) + + def join_selected_lines(self, separator: str = " ") -> None: + """ + Join the selected lines. + """ + assert self.selection_state + + # Get lines. + from_, to = sorted( + [self.cursor_position, self.selection_state.original_cursor_position] + ) + + before = self.text[:from_] + lines = self.text[from_:to].splitlines() + after = self.text[to:] + + # Replace leading spaces with just one space. + lines = [l.lstrip(" ") + separator for l in lines] + + # Set new document. + self.document = Document( + text=before + "".join(lines) + after, + cursor_position=len(before + "".join(lines[:-1])) - 1, + ) + + def swap_characters_before_cursor(self) -> None: + """ + Swap the last two characters before the cursor. + """ + pos = self.cursor_position + + if pos >= 2: + a = self.text[pos - 2] + b = self.text[pos - 1] + + self.text = self.text[: pos - 2] + b + a + self.text[pos:] + + def go_to_history(self, index: int) -> None: + """ + Go to this item in the history. + """ + if index < len(self._working_lines): + self.working_index = index + self.cursor_position = len(self.text) + + def complete_next(self, count: int = 1, disable_wrap_around: bool = False) -> None: + """ + Browse to the next completions. + (Does nothing if there are no completion.) + """ + index: Optional[int] + + if self.complete_state: + completions_count = len(self.complete_state.completions) + + if self.complete_state.complete_index is None: + index = 0 + elif self.complete_state.complete_index == completions_count - 1: + index = None + + if disable_wrap_around: + return + else: + index = min( + completions_count - 1, self.complete_state.complete_index + count + ) + self.go_to_completion(index) + + def complete_previous( + self, count: int = 1, disable_wrap_around: bool = False + ) -> None: + """ + Browse to the previous completions. + (Does nothing if there are no completion.) + """ + index: Optional[int] + + if self.complete_state: + if self.complete_state.complete_index == 0: + index = None + + if disable_wrap_around: + return + elif self.complete_state.complete_index is None: + index = len(self.complete_state.completions) - 1 + else: + index = max(0, self.complete_state.complete_index - count) + + self.go_to_completion(index) + + def cancel_completion(self) -> None: + """ + Cancel completion, go back to the original text. + """ + if self.complete_state: + self.go_to_completion(None) + self.complete_state = None + + def _set_completions(self, completions: List[Completion]) -> CompletionState: + """ + Start completions. (Generate list of completions and initialize.) + + By default, no completion will be selected. + """ + self.complete_state = CompletionState( + original_document=self.document, completions=completions + ) + + # Trigger event. This should eventually invalidate the layout. + self.on_completions_changed.fire() + + return self.complete_state + + def start_history_lines_completion(self) -> None: + """ + Start a completion based on all the other lines in the document and the + history. + """ + found_completions: Set[str] = set() + completions = [] + + # For every line of the whole history, find matches with the current line. + current_line = self.document.current_line_before_cursor.lstrip() + + for i, string in enumerate(self._working_lines): + for j, l in enumerate(string.split("\n")): + l = l.strip() + if l and l.startswith(current_line): + # When a new line has been found. + if l not in found_completions: + found_completions.add(l) + + # Create completion. + if i == self.working_index: + display_meta = "Current, line %s" % (j + 1) + else: + display_meta = "History %s, line %s" % (i + 1, j + 1) + + completions.append( + Completion( text=l, - start_position=-len(current_line), - display_meta=display_meta, - ) - ) - - self._set_completions(completions=completions[::-1]) - self.go_to_completion(0) - - def go_to_completion(self, index: Optional[int]) -> None: - """ - Select a completion from the list of current completions. - """ - assert self.complete_state - - # Set new completion - state = self.complete_state - state.go_to_index(index) - - # Set text/cursor position - new_text, new_cursor_position = state.new_text_and_position() - self.document = Document(new_text, new_cursor_position) - - # (changing text/cursor position will unset complete_state.) - self.complete_state = state - - def apply_completion(self, completion: Completion) -> None: - """ - Insert a given completion. - """ - # If there was already a completion active, cancel that one. - if self.complete_state: - self.go_to_completion(None) - self.complete_state = None - - # Insert text from the given completion. - self.delete_before_cursor(-completion.start_position) - self.insert_text(completion.text) - - def _set_history_search(self) -> None: - """ - Set `history_search_text`. - (The text before the cursor will be used for filtering the history.) - """ - if self.enable_history_search(): - if self.history_search_text is None: - self.history_search_text = self.document.text_before_cursor - else: - self.history_search_text = None - - def _history_matches(self, i: int) -> bool: - """ - True when the current entry matches the history search. - (when we don't have history search, it's also True.) - """ - return self.history_search_text is None or self._working_lines[i].startswith( - self.history_search_text - ) - - def history_forward(self, count: int = 1) -> None: - """ - Move forwards through the history. - - :param count: Amount of items to move forward. - """ - self._set_history_search() - - # Go forward in history. - found_something = False - - for i in range(self.working_index + 1, len(self._working_lines)): - if self._history_matches(i): - self.working_index = i - count -= 1 - found_something = True - if count == 0: - break - - # If we found an entry, move cursor to the end of the first line. - if found_something: - self.cursor_position = 0 - self.cursor_position += self.document.get_end_of_line_position() - - def history_backward(self, count: int = 1) -> None: - """ - Move backwards through history. - """ - self._set_history_search() - - # Go back in history. - found_something = False - - for i in range(self.working_index - 1, -1, -1): - if self._history_matches(i): - self.working_index = i - count -= 1 - found_something = True - if count == 0: - break - - # If we move to another entry, move cursor to the end of the line. - if found_something: - self.cursor_position = len(self.text) - - def yank_nth_arg( - self, n: Optional[int] = None, _yank_last_arg: bool = False - ) -> None: - """ - Pick nth word from previous history entry (depending on current - `yank_nth_arg_state`) and insert it at current position. Rotate through - history if called repeatedly. If no `n` has been given, take the first - argument. (The second word.) - - :param n: (None or int), The index of the word from the previous line - to take. - """ - assert n is None or isinstance(n, int) - history_strings = self.history.get_strings() - - if not len(history_strings): - return - - # Make sure we have a `YankNthArgState`. - if self.yank_nth_arg_state is None: - state = YankNthArgState(n=-1 if _yank_last_arg else 1) - else: - state = self.yank_nth_arg_state - - if n is not None: - state.n = n - - # Get new history position. - new_pos = state.history_position - 1 - if -new_pos > len(history_strings): - new_pos = -1 - - # Take argument from line. - line = history_strings[new_pos] - - words = [w.strip() for w in _QUOTED_WORDS_RE.split(line)] - words = [w for w in words if w] - try: - word = words[state.n] - except IndexError: - word = "" - - # Insert new argument. - if state.previous_inserted_word: - self.delete_before_cursor(len(state.previous_inserted_word)) - self.insert_text(word) - - # Save state again for next completion. (Note that the 'insert' - # operation from above clears `self.yank_nth_arg_state`.) - state.previous_inserted_word = word - state.history_position = new_pos - self.yank_nth_arg_state = state - - def yank_last_arg(self, n: Optional[int] = None) -> None: - """ - Like `yank_nth_arg`, but if no argument has been given, yank the last - word by default. - """ - self.yank_nth_arg(n=n, _yank_last_arg=True) - - def start_selection( - self, selection_type: SelectionType = SelectionType.CHARACTERS - ) -> None: - """ - Take the current cursor position as the start of this selection. - """ - self.selection_state = SelectionState(self.cursor_position, selection_type) - - def copy_selection(self, _cut: bool = False) -> ClipboardData: - """ - Copy selected text and return :class:`.ClipboardData` instance. - - Notice that this doesn't store the copied data on the clipboard yet. - You can store it like this: - - .. code:: python - - data = buffer.copy_selection() - get_app().clipboard.set_data(data) - """ - new_document, clipboard_data = self.document.cut_selection() - if _cut: - self.document = new_document - - self.selection_state = None - return clipboard_data - - def cut_selection(self) -> ClipboardData: - """ - Delete selected text and return :class:`.ClipboardData` instance. - """ - return self.copy_selection(_cut=True) - - def paste_clipboard_data( - self, - data: ClipboardData, - paste_mode: PasteMode = PasteMode.EMACS, - count: int = 1, - ) -> None: - """ - Insert the data from the clipboard. - """ - assert isinstance(data, ClipboardData) - assert paste_mode in (PasteMode.VI_BEFORE, PasteMode.VI_AFTER, PasteMode.EMACS) - - original_document = self.document - self.document = self.document.paste_clipboard_data( - data, paste_mode=paste_mode, count=count - ) - - # Remember original document. This assignment should come at the end, - # because assigning to 'document' will erase it. - self.document_before_paste = original_document - - def newline(self, copy_margin: bool = True) -> None: - """ - Insert a line ending at the current position. - """ - if copy_margin: - self.insert_text("\n" + self.document.leading_whitespace_in_current_line) - else: - self.insert_text("\n") - - def insert_line_above(self, copy_margin: bool = True) -> None: - """ - Insert a new line above the current one. - """ - if copy_margin: - insert = self.document.leading_whitespace_in_current_line + "\n" - else: - insert = "\n" - - self.cursor_position += self.document.get_start_of_line_position() - self.insert_text(insert) - self.cursor_position -= 1 - - def insert_line_below(self, copy_margin: bool = True) -> None: - """ - Insert a new line below the current one. - """ - if copy_margin: - insert = "\n" + self.document.leading_whitespace_in_current_line - else: - insert = "\n" - - self.cursor_position += self.document.get_end_of_line_position() - self.insert_text(insert) - - def insert_text( - self, - data: str, - overwrite: bool = False, - move_cursor: bool = True, - fire_event: bool = True, - ) -> None: - """ - Insert characters at cursor position. - - :param fire_event: Fire `on_text_insert` event. This is mainly used to - trigger autocompletion while typing. - """ - # Original text & cursor position. - otext = self.text - ocpos = self.cursor_position - - # In insert/text mode. - if overwrite: - # Don't overwrite the newline itself. Just before the line ending, - # it should act like insert mode. - overwritten_text = otext[ocpos : ocpos + len(data)] - if "\n" in overwritten_text: - overwritten_text = overwritten_text[: overwritten_text.find("\n")] - - text = otext[:ocpos] + data + otext[ocpos + len(overwritten_text) :] - else: - text = otext[:ocpos] + data + otext[ocpos:] - - if move_cursor: - cpos = self.cursor_position + len(data) - else: - cpos = self.cursor_position - - # Set new document. - # (Set text and cursor position at the same time. Otherwise, setting - # the text will fire a change event before the cursor position has been - # set. It works better to have this atomic.) - self.document = Document(text, cpos) - - # Fire 'on_text_insert' event. - if fire_event: # XXX: rename to `start_complete`. - self.on_text_insert.fire() - - # Only complete when "complete_while_typing" is enabled. - if self.completer and self.complete_while_typing(): - get_app().create_background_task(self._async_completer()) - - # Call auto_suggest. - if self.auto_suggest: - get_app().create_background_task(self._async_suggester()) - - def undo(self) -> None: - # Pop from the undo-stack until we find a text that if different from - # the current text. (The current logic of `save_to_undo_stack` will - # cause that the top of the undo stack is usually the same as the - # current text, so in that case we have to pop twice.) - while self._undo_stack: - text, pos = self._undo_stack.pop() - - if text != self.text: - # Push current text to redo stack. - self._redo_stack.append((self.text, self.cursor_position)) - - # Set new text/cursor_position. - self.document = Document(text, cursor_position=pos) - break - - def redo(self) -> None: - if self._redo_stack: - # Copy current state on undo stack. - self.save_to_undo_stack(clear_redo_stack=False) - - # Pop state from redo stack. - text, pos = self._redo_stack.pop() - self.document = Document(text, cursor_position=pos) - - def validate(self, set_cursor: bool = False) -> bool: - """ - Returns `True` if valid. - - :param set_cursor: Set the cursor position, if an error was found. - """ - # Don't call the validator again, if it was already called for the - # current input. - if self.validation_state != ValidationState.UNKNOWN: - return self.validation_state == ValidationState.VALID - - # Call validator. - if self.validator: - try: - self.validator.validate(self.document) - except ValidationError as e: - # Set cursor position (don't allow invalid values.) - if set_cursor: - self.cursor_position = min( - max(0, e.cursor_position), len(self.text) - ) - - self.validation_state = ValidationState.INVALID - self.validation_error = e - return False - - # Handle validation result. - self.validation_state = ValidationState.VALID - self.validation_error = None - return True - - async def _validate_async(self) -> None: - """ - Asynchronous version of `validate()`. - This one doesn't set the cursor position. - - We have both variants, because a synchronous version is required. - Handling the ENTER key needs to be completely synchronous, otherwise - stuff like type-ahead is going to give very weird results. (People - could type input while the ENTER key is still processed.) - - An asynchronous version is required if we have `validate_while_typing` - enabled. - """ - while True: - # Don't call the validator again, if it was already called for the - # current input. - if self.validation_state != ValidationState.UNKNOWN: - return - - # Call validator. - error = None - document = self.document - - if self.validator: - try: - await self.validator.validate_async(self.document) - except ValidationError as e: - error = e - - # If the document changed during the validation, try again. - if self.document != document: - continue - - # Handle validation result. - if error: - self.validation_state = ValidationState.INVALID - else: - self.validation_state = ValidationState.VALID - - self.validation_error = error - get_app().invalidate() # Trigger redraw (display error). - - def append_to_history(self) -> None: - """ - Append the current input to the history. - """ - # Save at the tail of the history. (But don't if the last entry the - # history is already the same.) - if self.text: - history_strings = self.history.get_strings() - if not len(history_strings) or history_strings[-1] != self.text: - self.history.append_string(self.text) - - def _search( - self, - search_state: SearchState, - include_current_position: bool = False, - count: int = 1, - ) -> Optional[Tuple[int, int]]: - """ - Execute search. Return (working_index, cursor_position) tuple when this - search is applied. Returns `None` when this text cannot be found. - """ - assert count > 0 - - text = search_state.text - direction = search_state.direction - ignore_case = search_state.ignore_case() - - def search_once( - working_index: int, document: Document - ) -> Optional[Tuple[int, Document]]: - """ - Do search one time. - Return (working_index, document) or `None` - """ - if direction == SearchDirection.FORWARD: - # Try find at the current input. - new_index = document.find( - text, - include_current_position=include_current_position, - ignore_case=ignore_case, - ) - - if new_index is not None: - return ( - working_index, - Document(document.text, document.cursor_position + new_index), - ) - else: - # No match, go forward in the history. (Include len+1 to wrap around.) - # (Here we should always include all cursor positions, because - # it's a different line.) - for i in range(working_index + 1, len(self._working_lines) + 1): - i %= len(self._working_lines) - - document = Document(self._working_lines[i], 0) - new_index = document.find( - text, include_current_position=True, ignore_case=ignore_case - ) - if new_index is not None: - return (i, Document(document.text, new_index)) - else: - # Try find at the current input. - new_index = document.find_backwards(text, ignore_case=ignore_case) - - if new_index is not None: - return ( - working_index, - Document(document.text, document.cursor_position + new_index), - ) - else: - # No match, go back in the history. (Include -1 to wrap around.) - for i in range(working_index - 1, -2, -1): - i %= len(self._working_lines) - - document = Document( - self._working_lines[i], len(self._working_lines[i]) - ) - new_index = document.find_backwards( - text, ignore_case=ignore_case - ) - if new_index is not None: - return ( - i, - Document(document.text, len(document.text) + new_index), - ) - return None - - # Do 'count' search iterations. - working_index = self.working_index - document = self.document - for _ in range(count): - result = search_once(working_index, document) - if result is None: - return None # Nothing found. - else: - working_index, document = result - - return (working_index, document.cursor_position) - - def document_for_search(self, search_state: SearchState) -> Document: - """ - Return a :class:`~prompt_toolkit.document.Document` instance that has - the text/cursor position for this search, if we would apply it. This - will be used in the - :class:`~prompt_toolkit.layout.BufferControl` to display feedback while - searching. - """ - search_result = self._search(search_state, include_current_position=True) - - if search_result is None: - return self.document - else: - working_index, cursor_position = search_result - - # Keep selection, when `working_index` was not changed. - if working_index == self.working_index: - selection = self.selection_state - else: - selection = None - - return Document( - self._working_lines[working_index], cursor_position, selection=selection - ) - - def get_search_position( - self, - search_state: SearchState, - include_current_position: bool = True, - count: int = 1, - ) -> int: - """ - Get the cursor position for this search. - (This operation won't change the `working_index`. It's won't go through - the history. Vi text objects can't span multiple items.) - """ - search_result = self._search( - search_state, include_current_position=include_current_position, count=count - ) - - if search_result is None: - return self.cursor_position - else: - working_index, cursor_position = search_result - return cursor_position - - def apply_search( - self, - search_state: SearchState, - include_current_position: bool = True, - count: int = 1, - ) -> None: - """ - Apply search. If something is found, set `working_index` and - `cursor_position`. - """ - search_result = self._search( - search_state, include_current_position=include_current_position, count=count - ) - - if search_result is not None: - working_index, cursor_position = search_result - self.working_index = working_index - self.cursor_position = cursor_position - - def exit_selection(self) -> None: - self.selection_state = None - - def _editor_simple_tempfile(self) -> Tuple[str, Callable[[], None]]: - """ - Simple (file) tempfile implementation. - Return (tempfile, cleanup_func). - """ - suffix = to_str(self.tempfile_suffix) - descriptor, filename = tempfile.mkstemp(suffix) - - os.write(descriptor, self.text.encode("utf-8")) - os.close(descriptor) - - def cleanup() -> None: - os.unlink(filename) - - return filename, cleanup - - def _editor_complex_tempfile(self) -> Tuple[str, Callable[[], None]]: - # Complex (directory) tempfile implementation. - headtail = to_str(self.tempfile) - if not headtail: - # Revert to simple case. - return self._editor_simple_tempfile() - headtail = str(headtail) - - # Try to make according to tempfile logic. - head, tail = os.path.split(headtail) - if os.path.isabs(head): - head = head[1:] - - dirpath = tempfile.mkdtemp() - if head: - dirpath = os.path.join(dirpath, head) - # Assume there is no issue creating dirs in this temp dir. - os.makedirs(dirpath) - - # Open the filename and write current text. - filename = os.path.join(dirpath, tail) - with open(filename, "w", encoding="utf-8") as fh: - fh.write(self.text) - - def cleanup() -> None: - shutil.rmtree(dirpath) - - return filename, cleanup - - def open_in_editor(self, validate_and_handle: bool = False) -> "asyncio.Task[None]": - """ - Open code in editor. - - This returns a future, and runs in a thread executor. - """ - if self.read_only(): - raise EditReadOnlyBuffer() - - # Write current text to temporary file - if self.tempfile: - filename, cleanup_func = self._editor_complex_tempfile() - else: - filename, cleanup_func = self._editor_simple_tempfile() - - async def run() -> None: - try: - # Open in editor - # (We need to use `run_in_terminal`, because not all editors go to - # the alternate screen buffer, and some could influence the cursor - # position.) - succes = await run_in_terminal( - lambda: self._open_file_in_editor(filename), in_executor=True - ) - - # Read content again. - if succes: - with open(filename, "rb") as f: - text = f.read().decode("utf-8") - - # Drop trailing newline. (Editors are supposed to add it at the - # end, but we don't need it.) - if text.endswith("\n"): - text = text[:-1] - - self.document = Document(text=text, cursor_position=len(text)) - - # Accept the input. - if validate_and_handle: - self.validate_and_handle() - - finally: - # Clean up temp dir/file. - cleanup_func() - - return get_app().create_background_task(run()) - - def _open_file_in_editor(self, filename: str) -> bool: - """ - Call editor executable. - - Return True when we received a zero return code. - """ - # If the 'VISUAL' or 'EDITOR' environment variable has been set, use that. - # Otherwise, fall back to the first available editor that we can find. - visual = os.environ.get("VISUAL") - editor = os.environ.get("EDITOR") - - editors = [ - visual, - editor, - # Order of preference. - "/usr/bin/editor", - "/usr/bin/nano", - "/usr/bin/pico", - "/usr/bin/vi", - "/usr/bin/emacs", - ] - - for e in editors: - if e: - try: - # Use 'shlex.split()', because $VISUAL can contain spaces - # and quotes. - returncode = subprocess.call(shlex.split(e) + [filename]) - return returncode == 0 - - except OSError: - # Executable does not exist, try the next one. - pass - - return False - - def start_completion( - self, - select_first: bool = False, - select_last: bool = False, - insert_common_part: bool = False, - complete_event: Optional[CompleteEvent] = None, - ) -> None: - """ - Start asynchronous autocompletion of this buffer. - (This will do nothing if a previous completion was still in progress.) - """ - # Only one of these options can be selected. - assert select_first + select_last + insert_common_part <= 1 - - get_app().create_background_task( - self._async_completer( - select_first=select_first, - select_last=select_last, - insert_common_part=insert_common_part, - complete_event=complete_event - or CompleteEvent(completion_requested=True), - ) - ) - - def _create_completer_coroutine(self) -> Callable[..., Awaitable[None]]: - """ - Create function for asynchronous autocompletion. - - (This consumes the asynchronous completer generator, which possibly - runs the completion algorithm in another thread.) - """ - - def completion_does_nothing(document: Document, completion: Completion) -> bool: - """ - Return `True` if applying this completion doesn't have any effect. - (When it doesn't insert any new text. - """ - text_before_cursor = document.text_before_cursor - replaced_text = text_before_cursor[ - len(text_before_cursor) + completion.start_position : - ] - return replaced_text == completion.text - - @_only_one_at_a_time - async def async_completer( - select_first: bool = False, - select_last: bool = False, - insert_common_part: bool = False, - complete_event: Optional[CompleteEvent] = None, - ) -> None: - - document = self.document - complete_event = complete_event or CompleteEvent(text_inserted=True) - - # Don't complete when we already have completions. - if self.complete_state or not self.completer: - return - - # Create an empty CompletionState. - complete_state = CompletionState(original_document=self.document) - self.complete_state = complete_state - - def proceed() -> bool: - """Keep retrieving completions. Input text has not yet changed - while generating completions.""" - return self.complete_state == complete_state - - async for completion in self.completer.get_completions_async( - document, complete_event - ): - complete_state.completions.append(completion) - self.on_completions_changed.fire() - - # If the input text changes, abort. - if not proceed(): - break - - completions = complete_state.completions - - # When there is only one completion, which has nothing to add, ignore it. - if len(completions) == 1 and completion_does_nothing( - document, completions[0] - ): - del completions[:] - - # Set completions if the text was not yet changed. - if proceed(): - # When no completions were found, or when the user selected - # already a completion by using the arrow keys, don't do anything. - if ( - not self.complete_state - or self.complete_state.complete_index is not None - ): - return - - # When there are no completions, reset completion state anyway. - if not completions: - self.complete_state = None - # Render the ui if the completion menu was shown - # it is needed especially if there is one completion and it was deleted. - self.on_completions_changed.fire() - return - - # Select first/last or insert common part, depending on the key - # binding. (For this we have to wait until all completions are - # loaded.) - - if select_first: - self.go_to_completion(0) - - elif select_last: - self.go_to_completion(len(completions) - 1) - - elif insert_common_part: - common_part = get_common_complete_suffix(document, completions) - if common_part: - # Insert the common part, update completions. - self.insert_text(common_part) - if len(completions) > 1: - # (Don't call `async_completer` again, but - # recalculate completions. See: - # https://github.com/ipython/ipython/issues/9658) - completions[:] = [ - c.new_completion_from_position(len(common_part)) - for c in completions - ] - - self._set_completions(completions=completions) - else: - self.complete_state = None - else: - # When we were asked to insert the "common" - # prefix, but there was no common suffix but - # still exactly one match, then select the - # first. (It could be that we have a completion - # which does * expansion, like '*.py', with - # exactly one match.) - if len(completions) == 1: - self.go_to_completion(0) - - else: - # If the last operation was an insert, (not a delete), restart - # the completion coroutine. - - if self.document.text_before_cursor == document.text_before_cursor: - return # Nothing changed. - - if self.document.text_before_cursor.startswith( - document.text_before_cursor - ): - raise _Retry - - return async_completer - - def _create_auto_suggest_coroutine(self) -> Callable[[], Awaitable[None]]: - """ - Create function for asynchronous auto suggestion. - (This can be in another thread.) - """ - - @_only_one_at_a_time - async def async_suggestor() -> None: - document = self.document - - # Don't suggest when we already have a suggestion. - if self.suggestion or not self.auto_suggest: - return - - suggestion = await self.auto_suggest.get_suggestion_async(self, document) - - # Set suggestion only if the text was not yet changed. - if self.document == document: - # Set suggestion and redraw interface. - self.suggestion = suggestion - self.on_suggestion_set.fire() - else: - # Otherwise, restart thread. - raise _Retry - - return async_suggestor - - def _create_auto_validate_coroutine(self) -> Callable[[], Awaitable[None]]: - """ - Create a function for asynchronous validation while typing. - (This can be in another thread.) - """ - - @_only_one_at_a_time - async def async_validator() -> None: - await self._validate_async() - - return async_validator - - def validate_and_handle(self) -> None: - """ - Validate buffer and handle the accept action. - """ - valid = self.validate(set_cursor=True) - - # When the validation succeeded, accept the input. - if valid: - if self.accept_handler: - keep_text = self.accept_handler(self) - else: - keep_text = False - - self.append_to_history() - - if not keep_text: - self.reset() - - -_T = TypeVar("_T", bound=Callable[..., Awaitable[None]]) - - -def _only_one_at_a_time(coroutine: _T) -> _T: - """ - Decorator that only starts the coroutine only if the previous call has - finished. (Used to make sure that we have only one autocompleter, auto - suggestor and validator running at a time.) - - When the coroutine raises `_Retry`, it is restarted. - """ - running = False - - @wraps(coroutine) - async def new_coroutine(*a: Any, **kw: Any) -> Any: - nonlocal running - - # Don't start a new function, if the previous is still in progress. - if running: - return - - running = True - - try: - while True: - try: - await coroutine(*a, **kw) - except _Retry: - continue - else: - return None - finally: - running = False - - return cast(_T, new_coroutine) - - -class _Retry(Exception): - "Retry in `_only_one_at_a_time`." - - -def indent(buffer: Buffer, from_row: int, to_row: int, count: int = 1) -> None: - """ - Indent text of a :class:`.Buffer` object. - """ - current_row = buffer.document.cursor_position_row - line_range = range(from_row, to_row) - - # Apply transformation. - new_text = buffer.transform_lines(line_range, lambda l: " " * count + l) - buffer.document = Document( - new_text, Document(new_text).translate_row_col_to_index(current_row, 0) - ) - - # Go to the start of the line. - buffer.cursor_position += buffer.document.get_start_of_line_position( - after_whitespace=True - ) - - -def unindent(buffer: Buffer, from_row: int, to_row: int, count: int = 1) -> None: - """ - Unindent text of a :class:`.Buffer` object. - """ - current_row = buffer.document.cursor_position_row - line_range = range(from_row, to_row) - - def transform(text: str) -> str: - remove = " " * count - if text.startswith(remove): - return text[len(remove) :] - else: - return text.lstrip() - - # Apply transformation. - new_text = buffer.transform_lines(line_range, transform) - buffer.document = Document( - new_text, Document(new_text).translate_row_col_to_index(current_row, 0) - ) - - # Go to the start of the line. - buffer.cursor_position += buffer.document.get_start_of_line_position( - after_whitespace=True - ) - - -def reshape_text(buffer: Buffer, from_row: int, to_row: int) -> None: - """ - Reformat text, taking the width into account. - `to_row` is included. - (Vi 'gq' operator.) - """ - lines = buffer.text.splitlines(True) - lines_before = lines[:from_row] - lines_after = lines[to_row + 1 :] - lines_to_reformat = lines[from_row : to_row + 1] - - if lines_to_reformat: - # Take indentation from the first line. - match = re.search(r"^\s*", lines_to_reformat[0]) - length = match.end() if match else 0 # `match` can't be None, actually. - - indent = lines_to_reformat[0][:length].replace("\n", "") - - # Now, take all the 'words' from the lines to be reshaped. - words = "".join(lines_to_reformat).split() - - # And reshape. - width = (buffer.text_width or 80) - len(indent) - reshaped_text = [indent] - current_width = 0 - for w in words: - if current_width: - if len(w) + current_width + 1 > width: - reshaped_text.append("\n") - reshaped_text.append(indent) - current_width = 0 - else: - reshaped_text.append(" ") - current_width += 1 - - reshaped_text.append(w) - current_width += len(w) - - if reshaped_text[-1] != "\n": - reshaped_text.append("\n") - - # Apply result. - buffer.document = Document( - text="".join(lines_before + reshaped_text + lines_after), - cursor_position=len("".join(lines_before + reshaped_text)), - ) + start_position=-len(current_line), + display_meta=display_meta, + ) + ) + + self._set_completions(completions=completions[::-1]) + self.go_to_completion(0) + + def go_to_completion(self, index: Optional[int]) -> None: + """ + Select a completion from the list of current completions. + """ + assert self.complete_state + + # Set new completion + state = self.complete_state + state.go_to_index(index) + + # Set text/cursor position + new_text, new_cursor_position = state.new_text_and_position() + self.document = Document(new_text, new_cursor_position) + + # (changing text/cursor position will unset complete_state.) + self.complete_state = state + + def apply_completion(self, completion: Completion) -> None: + """ + Insert a given completion. + """ + # If there was already a completion active, cancel that one. + if self.complete_state: + self.go_to_completion(None) + self.complete_state = None + + # Insert text from the given completion. + self.delete_before_cursor(-completion.start_position) + self.insert_text(completion.text) + + def _set_history_search(self) -> None: + """ + Set `history_search_text`. + (The text before the cursor will be used for filtering the history.) + """ + if self.enable_history_search(): + if self.history_search_text is None: + self.history_search_text = self.document.text_before_cursor + else: + self.history_search_text = None + + def _history_matches(self, i: int) -> bool: + """ + True when the current entry matches the history search. + (when we don't have history search, it's also True.) + """ + return self.history_search_text is None or self._working_lines[i].startswith( + self.history_search_text + ) + + def history_forward(self, count: int = 1) -> None: + """ + Move forwards through the history. + + :param count: Amount of items to move forward. + """ + self._set_history_search() + + # Go forward in history. + found_something = False + + for i in range(self.working_index + 1, len(self._working_lines)): + if self._history_matches(i): + self.working_index = i + count -= 1 + found_something = True + if count == 0: + break + + # If we found an entry, move cursor to the end of the first line. + if found_something: + self.cursor_position = 0 + self.cursor_position += self.document.get_end_of_line_position() + + def history_backward(self, count: int = 1) -> None: + """ + Move backwards through history. + """ + self._set_history_search() + + # Go back in history. + found_something = False + + for i in range(self.working_index - 1, -1, -1): + if self._history_matches(i): + self.working_index = i + count -= 1 + found_something = True + if count == 0: + break + + # If we move to another entry, move cursor to the end of the line. + if found_something: + self.cursor_position = len(self.text) + + def yank_nth_arg( + self, n: Optional[int] = None, _yank_last_arg: bool = False + ) -> None: + """ + Pick nth word from previous history entry (depending on current + `yank_nth_arg_state`) and insert it at current position. Rotate through + history if called repeatedly. If no `n` has been given, take the first + argument. (The second word.) + + :param n: (None or int), The index of the word from the previous line + to take. + """ + assert n is None or isinstance(n, int) + history_strings = self.history.get_strings() + + if not len(history_strings): + return + + # Make sure we have a `YankNthArgState`. + if self.yank_nth_arg_state is None: + state = YankNthArgState(n=-1 if _yank_last_arg else 1) + else: + state = self.yank_nth_arg_state + + if n is not None: + state.n = n + + # Get new history position. + new_pos = state.history_position - 1 + if -new_pos > len(history_strings): + new_pos = -1 + + # Take argument from line. + line = history_strings[new_pos] + + words = [w.strip() for w in _QUOTED_WORDS_RE.split(line)] + words = [w for w in words if w] + try: + word = words[state.n] + except IndexError: + word = "" + + # Insert new argument. + if state.previous_inserted_word: + self.delete_before_cursor(len(state.previous_inserted_word)) + self.insert_text(word) + + # Save state again for next completion. (Note that the 'insert' + # operation from above clears `self.yank_nth_arg_state`.) + state.previous_inserted_word = word + state.history_position = new_pos + self.yank_nth_arg_state = state + + def yank_last_arg(self, n: Optional[int] = None) -> None: + """ + Like `yank_nth_arg`, but if no argument has been given, yank the last + word by default. + """ + self.yank_nth_arg(n=n, _yank_last_arg=True) + + def start_selection( + self, selection_type: SelectionType = SelectionType.CHARACTERS + ) -> None: + """ + Take the current cursor position as the start of this selection. + """ + self.selection_state = SelectionState(self.cursor_position, selection_type) + + def copy_selection(self, _cut: bool = False) -> ClipboardData: + """ + Copy selected text and return :class:`.ClipboardData` instance. + + Notice that this doesn't store the copied data on the clipboard yet. + You can store it like this: + + .. code:: python + + data = buffer.copy_selection() + get_app().clipboard.set_data(data) + """ + new_document, clipboard_data = self.document.cut_selection() + if _cut: + self.document = new_document + + self.selection_state = None + return clipboard_data + + def cut_selection(self) -> ClipboardData: + """ + Delete selected text and return :class:`.ClipboardData` instance. + """ + return self.copy_selection(_cut=True) + + def paste_clipboard_data( + self, + data: ClipboardData, + paste_mode: PasteMode = PasteMode.EMACS, + count: int = 1, + ) -> None: + """ + Insert the data from the clipboard. + """ + assert isinstance(data, ClipboardData) + assert paste_mode in (PasteMode.VI_BEFORE, PasteMode.VI_AFTER, PasteMode.EMACS) + + original_document = self.document + self.document = self.document.paste_clipboard_data( + data, paste_mode=paste_mode, count=count + ) + + # Remember original document. This assignment should come at the end, + # because assigning to 'document' will erase it. + self.document_before_paste = original_document + + def newline(self, copy_margin: bool = True) -> None: + """ + Insert a line ending at the current position. + """ + if copy_margin: + self.insert_text("\n" + self.document.leading_whitespace_in_current_line) + else: + self.insert_text("\n") + + def insert_line_above(self, copy_margin: bool = True) -> None: + """ + Insert a new line above the current one. + """ + if copy_margin: + insert = self.document.leading_whitespace_in_current_line + "\n" + else: + insert = "\n" + + self.cursor_position += self.document.get_start_of_line_position() + self.insert_text(insert) + self.cursor_position -= 1 + + def insert_line_below(self, copy_margin: bool = True) -> None: + """ + Insert a new line below the current one. + """ + if copy_margin: + insert = "\n" + self.document.leading_whitespace_in_current_line + else: + insert = "\n" + + self.cursor_position += self.document.get_end_of_line_position() + self.insert_text(insert) + + def insert_text( + self, + data: str, + overwrite: bool = False, + move_cursor: bool = True, + fire_event: bool = True, + ) -> None: + """ + Insert characters at cursor position. + + :param fire_event: Fire `on_text_insert` event. This is mainly used to + trigger autocompletion while typing. + """ + # Original text & cursor position. + otext = self.text + ocpos = self.cursor_position + + # In insert/text mode. + if overwrite: + # Don't overwrite the newline itself. Just before the line ending, + # it should act like insert mode. + overwritten_text = otext[ocpos : ocpos + len(data)] + if "\n" in overwritten_text: + overwritten_text = overwritten_text[: overwritten_text.find("\n")] + + text = otext[:ocpos] + data + otext[ocpos + len(overwritten_text) :] + else: + text = otext[:ocpos] + data + otext[ocpos:] + + if move_cursor: + cpos = self.cursor_position + len(data) + else: + cpos = self.cursor_position + + # Set new document. + # (Set text and cursor position at the same time. Otherwise, setting + # the text will fire a change event before the cursor position has been + # set. It works better to have this atomic.) + self.document = Document(text, cpos) + + # Fire 'on_text_insert' event. + if fire_event: # XXX: rename to `start_complete`. + self.on_text_insert.fire() + + # Only complete when "complete_while_typing" is enabled. + if self.completer and self.complete_while_typing(): + get_app().create_background_task(self._async_completer()) + + # Call auto_suggest. + if self.auto_suggest: + get_app().create_background_task(self._async_suggester()) + + def undo(self) -> None: + # Pop from the undo-stack until we find a text that if different from + # the current text. (The current logic of `save_to_undo_stack` will + # cause that the top of the undo stack is usually the same as the + # current text, so in that case we have to pop twice.) + while self._undo_stack: + text, pos = self._undo_stack.pop() + + if text != self.text: + # Push current text to redo stack. + self._redo_stack.append((self.text, self.cursor_position)) + + # Set new text/cursor_position. + self.document = Document(text, cursor_position=pos) + break + + def redo(self) -> None: + if self._redo_stack: + # Copy current state on undo stack. + self.save_to_undo_stack(clear_redo_stack=False) + + # Pop state from redo stack. + text, pos = self._redo_stack.pop() + self.document = Document(text, cursor_position=pos) + + def validate(self, set_cursor: bool = False) -> bool: + """ + Returns `True` if valid. + + :param set_cursor: Set the cursor position, if an error was found. + """ + # Don't call the validator again, if it was already called for the + # current input. + if self.validation_state != ValidationState.UNKNOWN: + return self.validation_state == ValidationState.VALID + + # Call validator. + if self.validator: + try: + self.validator.validate(self.document) + except ValidationError as e: + # Set cursor position (don't allow invalid values.) + if set_cursor: + self.cursor_position = min( + max(0, e.cursor_position), len(self.text) + ) + + self.validation_state = ValidationState.INVALID + self.validation_error = e + return False + + # Handle validation result. + self.validation_state = ValidationState.VALID + self.validation_error = None + return True + + async def _validate_async(self) -> None: + """ + Asynchronous version of `validate()`. + This one doesn't set the cursor position. + + We have both variants, because a synchronous version is required. + Handling the ENTER key needs to be completely synchronous, otherwise + stuff like type-ahead is going to give very weird results. (People + could type input while the ENTER key is still processed.) + + An asynchronous version is required if we have `validate_while_typing` + enabled. + """ + while True: + # Don't call the validator again, if it was already called for the + # current input. + if self.validation_state != ValidationState.UNKNOWN: + return + + # Call validator. + error = None + document = self.document + + if self.validator: + try: + await self.validator.validate_async(self.document) + except ValidationError as e: + error = e + + # If the document changed during the validation, try again. + if self.document != document: + continue + + # Handle validation result. + if error: + self.validation_state = ValidationState.INVALID + else: + self.validation_state = ValidationState.VALID + + self.validation_error = error + get_app().invalidate() # Trigger redraw (display error). + + def append_to_history(self) -> None: + """ + Append the current input to the history. + """ + # Save at the tail of the history. (But don't if the last entry the + # history is already the same.) + if self.text: + history_strings = self.history.get_strings() + if not len(history_strings) or history_strings[-1] != self.text: + self.history.append_string(self.text) + + def _search( + self, + search_state: SearchState, + include_current_position: bool = False, + count: int = 1, + ) -> Optional[Tuple[int, int]]: + """ + Execute search. Return (working_index, cursor_position) tuple when this + search is applied. Returns `None` when this text cannot be found. + """ + assert count > 0 + + text = search_state.text + direction = search_state.direction + ignore_case = search_state.ignore_case() + + def search_once( + working_index: int, document: Document + ) -> Optional[Tuple[int, Document]]: + """ + Do search one time. + Return (working_index, document) or `None` + """ + if direction == SearchDirection.FORWARD: + # Try find at the current input. + new_index = document.find( + text, + include_current_position=include_current_position, + ignore_case=ignore_case, + ) + + if new_index is not None: + return ( + working_index, + Document(document.text, document.cursor_position + new_index), + ) + else: + # No match, go forward in the history. (Include len+1 to wrap around.) + # (Here we should always include all cursor positions, because + # it's a different line.) + for i in range(working_index + 1, len(self._working_lines) + 1): + i %= len(self._working_lines) + + document = Document(self._working_lines[i], 0) + new_index = document.find( + text, include_current_position=True, ignore_case=ignore_case + ) + if new_index is not None: + return (i, Document(document.text, new_index)) + else: + # Try find at the current input. + new_index = document.find_backwards(text, ignore_case=ignore_case) + + if new_index is not None: + return ( + working_index, + Document(document.text, document.cursor_position + new_index), + ) + else: + # No match, go back in the history. (Include -1 to wrap around.) + for i in range(working_index - 1, -2, -1): + i %= len(self._working_lines) + + document = Document( + self._working_lines[i], len(self._working_lines[i]) + ) + new_index = document.find_backwards( + text, ignore_case=ignore_case + ) + if new_index is not None: + return ( + i, + Document(document.text, len(document.text) + new_index), + ) + return None + + # Do 'count' search iterations. + working_index = self.working_index + document = self.document + for _ in range(count): + result = search_once(working_index, document) + if result is None: + return None # Nothing found. + else: + working_index, document = result + + return (working_index, document.cursor_position) + + def document_for_search(self, search_state: SearchState) -> Document: + """ + Return a :class:`~prompt_toolkit.document.Document` instance that has + the text/cursor position for this search, if we would apply it. This + will be used in the + :class:`~prompt_toolkit.layout.BufferControl` to display feedback while + searching. + """ + search_result = self._search(search_state, include_current_position=True) + + if search_result is None: + return self.document + else: + working_index, cursor_position = search_result + + # Keep selection, when `working_index` was not changed. + if working_index == self.working_index: + selection = self.selection_state + else: + selection = None + + return Document( + self._working_lines[working_index], cursor_position, selection=selection + ) + + def get_search_position( + self, + search_state: SearchState, + include_current_position: bool = True, + count: int = 1, + ) -> int: + """ + Get the cursor position for this search. + (This operation won't change the `working_index`. It's won't go through + the history. Vi text objects can't span multiple items.) + """ + search_result = self._search( + search_state, include_current_position=include_current_position, count=count + ) + + if search_result is None: + return self.cursor_position + else: + working_index, cursor_position = search_result + return cursor_position + + def apply_search( + self, + search_state: SearchState, + include_current_position: bool = True, + count: int = 1, + ) -> None: + """ + Apply search. If something is found, set `working_index` and + `cursor_position`. + """ + search_result = self._search( + search_state, include_current_position=include_current_position, count=count + ) + + if search_result is not None: + working_index, cursor_position = search_result + self.working_index = working_index + self.cursor_position = cursor_position + + def exit_selection(self) -> None: + self.selection_state = None + + def _editor_simple_tempfile(self) -> Tuple[str, Callable[[], None]]: + """ + Simple (file) tempfile implementation. + Return (tempfile, cleanup_func). + """ + suffix = to_str(self.tempfile_suffix) + descriptor, filename = tempfile.mkstemp(suffix) + + os.write(descriptor, self.text.encode("utf-8")) + os.close(descriptor) + + def cleanup() -> None: + os.unlink(filename) + + return filename, cleanup + + def _editor_complex_tempfile(self) -> Tuple[str, Callable[[], None]]: + # Complex (directory) tempfile implementation. + headtail = to_str(self.tempfile) + if not headtail: + # Revert to simple case. + return self._editor_simple_tempfile() + headtail = str(headtail) + + # Try to make according to tempfile logic. + head, tail = os.path.split(headtail) + if os.path.isabs(head): + head = head[1:] + + dirpath = tempfile.mkdtemp() + if head: + dirpath = os.path.join(dirpath, head) + # Assume there is no issue creating dirs in this temp dir. + os.makedirs(dirpath) + + # Open the filename and write current text. + filename = os.path.join(dirpath, tail) + with open(filename, "w", encoding="utf-8") as fh: + fh.write(self.text) + + def cleanup() -> None: + shutil.rmtree(dirpath) + + return filename, cleanup + + def open_in_editor(self, validate_and_handle: bool = False) -> "asyncio.Task[None]": + """ + Open code in editor. + + This returns a future, and runs in a thread executor. + """ + if self.read_only(): + raise EditReadOnlyBuffer() + + # Write current text to temporary file + if self.tempfile: + filename, cleanup_func = self._editor_complex_tempfile() + else: + filename, cleanup_func = self._editor_simple_tempfile() + + async def run() -> None: + try: + # Open in editor + # (We need to use `run_in_terminal`, because not all editors go to + # the alternate screen buffer, and some could influence the cursor + # position.) + succes = await run_in_terminal( + lambda: self._open_file_in_editor(filename), in_executor=True + ) + + # Read content again. + if succes: + with open(filename, "rb") as f: + text = f.read().decode("utf-8") + + # Drop trailing newline. (Editors are supposed to add it at the + # end, but we don't need it.) + if text.endswith("\n"): + text = text[:-1] + + self.document = Document(text=text, cursor_position=len(text)) + + # Accept the input. + if validate_and_handle: + self.validate_and_handle() + + finally: + # Clean up temp dir/file. + cleanup_func() + + return get_app().create_background_task(run()) + + def _open_file_in_editor(self, filename: str) -> bool: + """ + Call editor executable. + + Return True when we received a zero return code. + """ + # If the 'VISUAL' or 'EDITOR' environment variable has been set, use that. + # Otherwise, fall back to the first available editor that we can find. + visual = os.environ.get("VISUAL") + editor = os.environ.get("EDITOR") + + editors = [ + visual, + editor, + # Order of preference. + "/usr/bin/editor", + "/usr/bin/nano", + "/usr/bin/pico", + "/usr/bin/vi", + "/usr/bin/emacs", + ] + + for e in editors: + if e: + try: + # Use 'shlex.split()', because $VISUAL can contain spaces + # and quotes. + returncode = subprocess.call(shlex.split(e) + [filename]) + return returncode == 0 + + except OSError: + # Executable does not exist, try the next one. + pass + + return False + + def start_completion( + self, + select_first: bool = False, + select_last: bool = False, + insert_common_part: bool = False, + complete_event: Optional[CompleteEvent] = None, + ) -> None: + """ + Start asynchronous autocompletion of this buffer. + (This will do nothing if a previous completion was still in progress.) + """ + # Only one of these options can be selected. + assert select_first + select_last + insert_common_part <= 1 + + get_app().create_background_task( + self._async_completer( + select_first=select_first, + select_last=select_last, + insert_common_part=insert_common_part, + complete_event=complete_event + or CompleteEvent(completion_requested=True), + ) + ) + + def _create_completer_coroutine(self) -> Callable[..., Awaitable[None]]: + """ + Create function for asynchronous autocompletion. + + (This consumes the asynchronous completer generator, which possibly + runs the completion algorithm in another thread.) + """ + + def completion_does_nothing(document: Document, completion: Completion) -> bool: + """ + Return `True` if applying this completion doesn't have any effect. + (When it doesn't insert any new text. + """ + text_before_cursor = document.text_before_cursor + replaced_text = text_before_cursor[ + len(text_before_cursor) + completion.start_position : + ] + return replaced_text == completion.text + + @_only_one_at_a_time + async def async_completer( + select_first: bool = False, + select_last: bool = False, + insert_common_part: bool = False, + complete_event: Optional[CompleteEvent] = None, + ) -> None: + + document = self.document + complete_event = complete_event or CompleteEvent(text_inserted=True) + + # Don't complete when we already have completions. + if self.complete_state or not self.completer: + return + + # Create an empty CompletionState. + complete_state = CompletionState(original_document=self.document) + self.complete_state = complete_state + + def proceed() -> bool: + """Keep retrieving completions. Input text has not yet changed + while generating completions.""" + return self.complete_state == complete_state + + async for completion in self.completer.get_completions_async( + document, complete_event + ): + complete_state.completions.append(completion) + self.on_completions_changed.fire() + + # If the input text changes, abort. + if not proceed(): + break + + completions = complete_state.completions + + # When there is only one completion, which has nothing to add, ignore it. + if len(completions) == 1 and completion_does_nothing( + document, completions[0] + ): + del completions[:] + + # Set completions if the text was not yet changed. + if proceed(): + # When no completions were found, or when the user selected + # already a completion by using the arrow keys, don't do anything. + if ( + not self.complete_state + or self.complete_state.complete_index is not None + ): + return + + # When there are no completions, reset completion state anyway. + if not completions: + self.complete_state = None + # Render the ui if the completion menu was shown + # it is needed especially if there is one completion and it was deleted. + self.on_completions_changed.fire() + return + + # Select first/last or insert common part, depending on the key + # binding. (For this we have to wait until all completions are + # loaded.) + + if select_first: + self.go_to_completion(0) + + elif select_last: + self.go_to_completion(len(completions) - 1) + + elif insert_common_part: + common_part = get_common_complete_suffix(document, completions) + if common_part: + # Insert the common part, update completions. + self.insert_text(common_part) + if len(completions) > 1: + # (Don't call `async_completer` again, but + # recalculate completions. See: + # https://github.com/ipython/ipython/issues/9658) + completions[:] = [ + c.new_completion_from_position(len(common_part)) + for c in completions + ] + + self._set_completions(completions=completions) + else: + self.complete_state = None + else: + # When we were asked to insert the "common" + # prefix, but there was no common suffix but + # still exactly one match, then select the + # first. (It could be that we have a completion + # which does * expansion, like '*.py', with + # exactly one match.) + if len(completions) == 1: + self.go_to_completion(0) + + else: + # If the last operation was an insert, (not a delete), restart + # the completion coroutine. + + if self.document.text_before_cursor == document.text_before_cursor: + return # Nothing changed. + + if self.document.text_before_cursor.startswith( + document.text_before_cursor + ): + raise _Retry + + return async_completer + + def _create_auto_suggest_coroutine(self) -> Callable[[], Awaitable[None]]: + """ + Create function for asynchronous auto suggestion. + (This can be in another thread.) + """ + + @_only_one_at_a_time + async def async_suggestor() -> None: + document = self.document + + # Don't suggest when we already have a suggestion. + if self.suggestion or not self.auto_suggest: + return + + suggestion = await self.auto_suggest.get_suggestion_async(self, document) + + # Set suggestion only if the text was not yet changed. + if self.document == document: + # Set suggestion and redraw interface. + self.suggestion = suggestion + self.on_suggestion_set.fire() + else: + # Otherwise, restart thread. + raise _Retry + + return async_suggestor + + def _create_auto_validate_coroutine(self) -> Callable[[], Awaitable[None]]: + """ + Create a function for asynchronous validation while typing. + (This can be in another thread.) + """ + + @_only_one_at_a_time + async def async_validator() -> None: + await self._validate_async() + + return async_validator + + def validate_and_handle(self) -> None: + """ + Validate buffer and handle the accept action. + """ + valid = self.validate(set_cursor=True) + + # When the validation succeeded, accept the input. + if valid: + if self.accept_handler: + keep_text = self.accept_handler(self) + else: + keep_text = False + + self.append_to_history() + + if not keep_text: + self.reset() + + +_T = TypeVar("_T", bound=Callable[..., Awaitable[None]]) + + +def _only_one_at_a_time(coroutine: _T) -> _T: + """ + Decorator that only starts the coroutine only if the previous call has + finished. (Used to make sure that we have only one autocompleter, auto + suggestor and validator running at a time.) + + When the coroutine raises `_Retry`, it is restarted. + """ + running = False + + @wraps(coroutine) + async def new_coroutine(*a: Any, **kw: Any) -> Any: + nonlocal running + + # Don't start a new function, if the previous is still in progress. + if running: + return + + running = True + + try: + while True: + try: + await coroutine(*a, **kw) + except _Retry: + continue + else: + return None + finally: + running = False + + return cast(_T, new_coroutine) + + +class _Retry(Exception): + "Retry in `_only_one_at_a_time`." + + +def indent(buffer: Buffer, from_row: int, to_row: int, count: int = 1) -> None: + """ + Indent text of a :class:`.Buffer` object. + """ + current_row = buffer.document.cursor_position_row + line_range = range(from_row, to_row) + + # Apply transformation. + new_text = buffer.transform_lines(line_range, lambda l: " " * count + l) + buffer.document = Document( + new_text, Document(new_text).translate_row_col_to_index(current_row, 0) + ) + + # Go to the start of the line. + buffer.cursor_position += buffer.document.get_start_of_line_position( + after_whitespace=True + ) + + +def unindent(buffer: Buffer, from_row: int, to_row: int, count: int = 1) -> None: + """ + Unindent text of a :class:`.Buffer` object. + """ + current_row = buffer.document.cursor_position_row + line_range = range(from_row, to_row) + + def transform(text: str) -> str: + remove = " " * count + if text.startswith(remove): + return text[len(remove) :] + else: + return text.lstrip() + + # Apply transformation. + new_text = buffer.transform_lines(line_range, transform) + buffer.document = Document( + new_text, Document(new_text).translate_row_col_to_index(current_row, 0) + ) + + # Go to the start of the line. + buffer.cursor_position += buffer.document.get_start_of_line_position( + after_whitespace=True + ) + + +def reshape_text(buffer: Buffer, from_row: int, to_row: int) -> None: + """ + Reformat text, taking the width into account. + `to_row` is included. + (Vi 'gq' operator.) + """ + lines = buffer.text.splitlines(True) + lines_before = lines[:from_row] + lines_after = lines[to_row + 1 :] + lines_to_reformat = lines[from_row : to_row + 1] + + if lines_to_reformat: + # Take indentation from the first line. + match = re.search(r"^\s*", lines_to_reformat[0]) + length = match.end() if match else 0 # `match` can't be None, actually. + + indent = lines_to_reformat[0][:length].replace("\n", "") + + # Now, take all the 'words' from the lines to be reshaped. + words = "".join(lines_to_reformat).split() + + # And reshape. + width = (buffer.text_width or 80) - len(indent) + reshaped_text = [indent] + current_width = 0 + for w in words: + if current_width: + if len(w) + current_width + 1 > width: + reshaped_text.append("\n") + reshaped_text.append(indent) + current_width = 0 + else: + reshaped_text.append(" ") + current_width += 1 + + reshaped_text.append(w) + current_width += len(w) + + if reshaped_text[-1] != "\n": + reshaped_text.append("\n") + + # Apply result. + buffer.document = Document( + text="".join(lines_before + reshaped_text + lines_after), + cursor_position=len("".join(lines_before + reshaped_text)), + ) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/cache.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/cache.py index 07ea90d9cd..e5e9d70eca 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/cache.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/cache.py @@ -1,125 +1,125 @@ -from collections import deque -from functools import wraps -from typing import Any, Callable, Deque, Dict, Generic, Hashable, Tuple, TypeVar, cast - -__all__ = [ - "SimpleCache", - "FastDictCache", - "memoized", -] - -_T = TypeVar("_T", bound=Hashable) -_U = TypeVar("_U") - - -class SimpleCache(Generic[_T, _U]): - """ - Very simple cache that discards the oldest item when the cache size is - exceeded. - - :param maxsize: Maximum size of the cache. (Don't make it too big.) - """ - - def __init__(self, maxsize: int = 8) -> None: - assert maxsize > 0 - - self._data: Dict[_T, _U] = {} - self._keys: Deque[_T] = deque() - self.maxsize: int = maxsize - - def get(self, key: _T, getter_func: Callable[[], _U]) -> _U: - """ - Get object from the cache. - If not found, call `getter_func` to resolve it, and put that on the top - of the cache instead. - """ - # Look in cache first. - try: - return self._data[key] - except KeyError: - # Not found? Get it. - value = getter_func() - self._data[key] = value - self._keys.append(key) - - # Remove the oldest key when the size is exceeded. - if len(self._data) > self.maxsize: - key_to_remove = self._keys.popleft() - if key_to_remove in self._data: - del self._data[key_to_remove] - - return value - - def clear(self) -> None: - "Clear cache." - self._data = {} - self._keys = deque() - - -_K = TypeVar("_K", bound=Tuple[Hashable, ...]) -_V = TypeVar("_V") - - -class FastDictCache(Dict[_K, _V]): - """ - Fast, lightweight cache which keeps at most `size` items. - It will discard the oldest items in the cache first. - - The cache is a dictionary, which doesn't keep track of access counts. - It is perfect to cache little immutable objects which are not expensive to - create, but where a dictionary lookup is still much faster than an object - instantiation. - - :param get_value: Callable that's called in case of a missing key. - """ - - # NOTE: This cache is used to cache `prompt_toolkit.layout.screen.Char` and - # `prompt_toolkit.Document`. Make sure to keep this really lightweight. - # Accessing the cache should stay faster than instantiating new - # objects. - # (Dictionary lookups are really fast.) - # SimpleCache is still required for cases where the cache key is not - # the same as the arguments given to the function that creates the - # value.) - def __init__(self, get_value: Callable[..., _V], size: int = 1000000) -> None: - assert size > 0 - - self._keys: Deque[_K] = deque() - self.get_value = get_value - self.size = size - - def __missing__(self, key: _K) -> _V: - # Remove the oldest key when the size is exceeded. - if len(self) > self.size: - key_to_remove = self._keys.popleft() - if key_to_remove in self: - del self[key_to_remove] - - result = self.get_value(*key) - self[key] = result - self._keys.append(key) - return result - - -_F = TypeVar("_F", bound=Callable[..., object]) - - -def memoized(maxsize: int = 1024) -> Callable[[_F], _F]: - """ - Memoization decorator for immutable classes and pure functions. - """ - - def decorator(obj: _F) -> _F: - cache: SimpleCache[Hashable, Any] = SimpleCache(maxsize=maxsize) - - @wraps(obj) - def new_callable(*a: Any, **kw: Any) -> Any: - def create_new() -> Any: - return obj(*a, **kw) - - key = (a, tuple(sorted(kw.items()))) - return cache.get(key, create_new) - - return cast(_F, new_callable) - - return decorator +from collections import deque +from functools import wraps +from typing import Any, Callable, Deque, Dict, Generic, Hashable, Tuple, TypeVar, cast + +__all__ = [ + "SimpleCache", + "FastDictCache", + "memoized", +] + +_T = TypeVar("_T", bound=Hashable) +_U = TypeVar("_U") + + +class SimpleCache(Generic[_T, _U]): + """ + Very simple cache that discards the oldest item when the cache size is + exceeded. + + :param maxsize: Maximum size of the cache. (Don't make it too big.) + """ + + def __init__(self, maxsize: int = 8) -> None: + assert maxsize > 0 + + self._data: Dict[_T, _U] = {} + self._keys: Deque[_T] = deque() + self.maxsize: int = maxsize + + def get(self, key: _T, getter_func: Callable[[], _U]) -> _U: + """ + Get object from the cache. + If not found, call `getter_func` to resolve it, and put that on the top + of the cache instead. + """ + # Look in cache first. + try: + return self._data[key] + except KeyError: + # Not found? Get it. + value = getter_func() + self._data[key] = value + self._keys.append(key) + + # Remove the oldest key when the size is exceeded. + if len(self._data) > self.maxsize: + key_to_remove = self._keys.popleft() + if key_to_remove in self._data: + del self._data[key_to_remove] + + return value + + def clear(self) -> None: + "Clear cache." + self._data = {} + self._keys = deque() + + +_K = TypeVar("_K", bound=Tuple[Hashable, ...]) +_V = TypeVar("_V") + + +class FastDictCache(Dict[_K, _V]): + """ + Fast, lightweight cache which keeps at most `size` items. + It will discard the oldest items in the cache first. + + The cache is a dictionary, which doesn't keep track of access counts. + It is perfect to cache little immutable objects which are not expensive to + create, but where a dictionary lookup is still much faster than an object + instantiation. + + :param get_value: Callable that's called in case of a missing key. + """ + + # NOTE: This cache is used to cache `prompt_toolkit.layout.screen.Char` and + # `prompt_toolkit.Document`. Make sure to keep this really lightweight. + # Accessing the cache should stay faster than instantiating new + # objects. + # (Dictionary lookups are really fast.) + # SimpleCache is still required for cases where the cache key is not + # the same as the arguments given to the function that creates the + # value.) + def __init__(self, get_value: Callable[..., _V], size: int = 1000000) -> None: + assert size > 0 + + self._keys: Deque[_K] = deque() + self.get_value = get_value + self.size = size + + def __missing__(self, key: _K) -> _V: + # Remove the oldest key when the size is exceeded. + if len(self) > self.size: + key_to_remove = self._keys.popleft() + if key_to_remove in self: + del self[key_to_remove] + + result = self.get_value(*key) + self[key] = result + self._keys.append(key) + return result + + +_F = TypeVar("_F", bound=Callable[..., object]) + + +def memoized(maxsize: int = 1024) -> Callable[[_F], _F]: + """ + Memoization decorator for immutable classes and pure functions. + """ + + def decorator(obj: _F) -> _F: + cache: SimpleCache[Hashable, Any] = SimpleCache(maxsize=maxsize) + + @wraps(obj) + def new_callable(*a: Any, **kw: Any) -> Any: + def create_new() -> Any: + return obj(*a, **kw) + + key = (a, tuple(sorted(kw.items()))) + return cache.get(key, create_new) + + return cast(_F, new_callable) + + return decorator diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/clipboard/__init__.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/clipboard/__init__.py index 4311356ceb..160b50aca5 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/clipboard/__init__.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/clipboard/__init__.py @@ -1,15 +1,15 @@ -from .base import Clipboard, ClipboardData, DummyClipboard, DynamicClipboard -from .in_memory import InMemoryClipboard - -# We are not importing `PyperclipClipboard` here, because it would require the -# `pyperclip` module to be present. - -# from .pyperclip import PyperclipClipboard - -__all__ = [ - "Clipboard", - "ClipboardData", - "DummyClipboard", - "DynamicClipboard", - "InMemoryClipboard", -] +from .base import Clipboard, ClipboardData, DummyClipboard, DynamicClipboard +from .in_memory import InMemoryClipboard + +# We are not importing `PyperclipClipboard` here, because it would require the +# `pyperclip` module to be present. + +# from .pyperclip import PyperclipClipboard + +__all__ = [ + "Clipboard", + "ClipboardData", + "DummyClipboard", + "DynamicClipboard", + "InMemoryClipboard", +] diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/clipboard/base.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/clipboard/base.py index d492cf8cb0..c24623dacc 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/clipboard/base.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/clipboard/base.py @@ -1,107 +1,107 @@ -""" -Clipboard for command line interface. -""" -from abc import ABCMeta, abstractmethod -from typing import Callable, Optional - -from prompt_toolkit.selection import SelectionType - -__all__ = [ - "Clipboard", - "ClipboardData", - "DummyClipboard", - "DynamicClipboard", -] - - -class ClipboardData: - """ - Text on the clipboard. - - :param text: string - :param type: :class:`~prompt_toolkit.selection.SelectionType` - """ - - def __init__( - self, text: str = "", type: SelectionType = SelectionType.CHARACTERS - ) -> None: - - self.text = text - self.type = type - - -class Clipboard(metaclass=ABCMeta): - """ - Abstract baseclass for clipboards. - (An implementation can be in memory, it can share the X11 or Windows - keyboard, or can be persistent.) - """ - - @abstractmethod - def set_data(self, data: ClipboardData) -> None: - """ - Set data to the clipboard. - - :param data: :class:`~.ClipboardData` instance. - """ - - def set_text(self, text: str) -> None: # Not abstract. - """ - Shortcut for setting plain text on clipboard. - """ - self.set_data(ClipboardData(text)) - - def rotate(self) -> None: - """ - For Emacs mode, rotate the kill ring. - """ - - @abstractmethod - def get_data(self) -> ClipboardData: - """ - Return clipboard data. - """ - - -class DummyClipboard(Clipboard): - """ - Clipboard implementation that doesn't remember anything. - """ - - def set_data(self, data: ClipboardData) -> None: - pass - - def set_text(self, text: str) -> None: - pass - - def rotate(self) -> None: - pass - - def get_data(self) -> ClipboardData: - return ClipboardData() - - -class DynamicClipboard(Clipboard): - """ - Clipboard class that can dynamically returns any Clipboard. - - :param get_clipboard: Callable that returns a :class:`.Clipboard` instance. - """ - - def __init__(self, get_clipboard: Callable[[], Optional[Clipboard]]) -> None: - self.get_clipboard = get_clipboard - - def _clipboard(self) -> Clipboard: - return self.get_clipboard() or DummyClipboard() - - def set_data(self, data: ClipboardData) -> None: - self._clipboard().set_data(data) - - def set_text(self, text: str) -> None: - self._clipboard().set_text(text) - - def rotate(self) -> None: - self._clipboard().rotate() - - def get_data(self) -> ClipboardData: - return self._clipboard().get_data() +""" +Clipboard for command line interface. +""" +from abc import ABCMeta, abstractmethod +from typing import Callable, Optional + +from prompt_toolkit.selection import SelectionType + +__all__ = [ + "Clipboard", + "ClipboardData", + "DummyClipboard", + "DynamicClipboard", +] + + +class ClipboardData: + """ + Text on the clipboard. + + :param text: string + :param type: :class:`~prompt_toolkit.selection.SelectionType` + """ + + def __init__( + self, text: str = "", type: SelectionType = SelectionType.CHARACTERS + ) -> None: + + self.text = text + self.type = type + + +class Clipboard(metaclass=ABCMeta): + """ + Abstract baseclass for clipboards. + (An implementation can be in memory, it can share the X11 or Windows + keyboard, or can be persistent.) + """ + + @abstractmethod + def set_data(self, data: ClipboardData) -> None: + """ + Set data to the clipboard. + + :param data: :class:`~.ClipboardData` instance. + """ + + def set_text(self, text: str) -> None: # Not abstract. + """ + Shortcut for setting plain text on clipboard. + """ + self.set_data(ClipboardData(text)) + + def rotate(self) -> None: + """ + For Emacs mode, rotate the kill ring. + """ + + @abstractmethod + def get_data(self) -> ClipboardData: + """ + Return clipboard data. + """ + + +class DummyClipboard(Clipboard): + """ + Clipboard implementation that doesn't remember anything. + """ + + def set_data(self, data: ClipboardData) -> None: + pass + + def set_text(self, text: str) -> None: + pass + + def rotate(self) -> None: + pass + + def get_data(self) -> ClipboardData: + return ClipboardData() + + +class DynamicClipboard(Clipboard): + """ + Clipboard class that can dynamically returns any Clipboard. + + :param get_clipboard: Callable that returns a :class:`.Clipboard` instance. + """ + + def __init__(self, get_clipboard: Callable[[], Optional[Clipboard]]) -> None: + self.get_clipboard = get_clipboard + + def _clipboard(self) -> Clipboard: + return self.get_clipboard() or DummyClipboard() + + def set_data(self, data: ClipboardData) -> None: + self._clipboard().set_data(data) + + def set_text(self, text: str) -> None: + self._clipboard().set_text(text) + + def rotate(self) -> None: + self._clipboard().rotate() + + def get_data(self) -> ClipboardData: + return self._clipboard().get_data() diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/clipboard/in_memory.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/clipboard/in_memory.py index 35cb564570..1902e36a55 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/clipboard/in_memory.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/clipboard/in_memory.py @@ -1,46 +1,46 @@ -from collections import deque -from typing import Deque, Optional - -from .base import Clipboard, ClipboardData - -__all__ = [ - "InMemoryClipboard", -] - - -class InMemoryClipboard(Clipboard): - """ - Default clipboard implementation. - Just keep the data in memory. - - This implements a kill-ring, for Emacs mode. - """ - - def __init__( - self, data: Optional[ClipboardData] = None, max_size: int = 60 - ) -> None: - - assert max_size >= 1 - - self.max_size = max_size - self._ring: Deque[ClipboardData] = deque() - - if data is not None: - self.set_data(data) - - def set_data(self, data: ClipboardData) -> None: - self._ring.appendleft(data) - - while len(self._ring) > self.max_size: - self._ring.pop() - - def get_data(self) -> ClipboardData: - if self._ring: - return self._ring[0] - else: - return ClipboardData() - - def rotate(self) -> None: - if self._ring: - # Add the very first item at the end. - self._ring.append(self._ring.popleft()) +from collections import deque +from typing import Deque, Optional + +from .base import Clipboard, ClipboardData + +__all__ = [ + "InMemoryClipboard", +] + + +class InMemoryClipboard(Clipboard): + """ + Default clipboard implementation. + Just keep the data in memory. + + This implements a kill-ring, for Emacs mode. + """ + + def __init__( + self, data: Optional[ClipboardData] = None, max_size: int = 60 + ) -> None: + + assert max_size >= 1 + + self.max_size = max_size + self._ring: Deque[ClipboardData] = deque() + + if data is not None: + self.set_data(data) + + def set_data(self, data: ClipboardData) -> None: + self._ring.appendleft(data) + + while len(self._ring) > self.max_size: + self._ring.pop() + + def get_data(self) -> ClipboardData: + if self._ring: + return self._ring[0] + else: + return ClipboardData() + + def rotate(self) -> None: + if self._ring: + # Add the very first item at the end. + self._ring.append(self._ring.popleft()) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/clipboard/pyperclip.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/clipboard/pyperclip.py index 5491e51bfe..c8cb7afb67 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/clipboard/pyperclip.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/clipboard/pyperclip.py @@ -1,42 +1,42 @@ -from typing import Optional - -import pyperclip - -from prompt_toolkit.selection import SelectionType - -from .base import Clipboard, ClipboardData - -__all__ = [ - "PyperclipClipboard", -] - - -class PyperclipClipboard(Clipboard): - """ - Clipboard that synchronizes with the Windows/Mac/Linux system clipboard, - using the pyperclip module. - """ - - def __init__(self) -> None: - self._data: Optional[ClipboardData] = None - - def set_data(self, data: ClipboardData) -> None: - self._data = data - pyperclip.copy(data.text) - - def get_data(self) -> ClipboardData: - text = pyperclip.paste() - - # When the clipboard data is equal to what we copied last time, reuse - # the `ClipboardData` instance. That way we're sure to keep the same - # `SelectionType`. - if self._data and self._data.text == text: - return self._data - - # Pyperclip returned something else. Create a new `ClipboardData` - # instance. - else: - return ClipboardData( - text=text, - type=SelectionType.LINES if "\n" in text else SelectionType.CHARACTERS, - ) +from typing import Optional + +import pyperclip + +from prompt_toolkit.selection import SelectionType + +from .base import Clipboard, ClipboardData + +__all__ = [ + "PyperclipClipboard", +] + + +class PyperclipClipboard(Clipboard): + """ + Clipboard that synchronizes with the Windows/Mac/Linux system clipboard, + using the pyperclip module. + """ + + def __init__(self) -> None: + self._data: Optional[ClipboardData] = None + + def set_data(self, data: ClipboardData) -> None: + self._data = data + pyperclip.copy(data.text) + + def get_data(self) -> ClipboardData: + text = pyperclip.paste() + + # When the clipboard data is equal to what we copied last time, reuse + # the `ClipboardData` instance. That way we're sure to keep the same + # `SelectionType`. + if self._data and self._data.text == text: + return self._data + + # Pyperclip returned something else. Create a new `ClipboardData` + # instance. + else: + return ClipboardData( + text=text, + type=SelectionType.LINES if "\n" in text else SelectionType.CHARACTERS, + ) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/__init__.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/__init__.py index 0b8e86e76e..270c74337c 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/__init__.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/__init__.py @@ -1,41 +1,41 @@ -from .base import ( - CompleteEvent, - Completer, - Completion, - ConditionalCompleter, - DummyCompleter, - DynamicCompleter, - ThreadedCompleter, - get_common_complete_suffix, - merge_completers, -) -from .deduplicate import DeduplicateCompleter -from .filesystem import ExecutableCompleter, PathCompleter -from .fuzzy_completer import FuzzyCompleter, FuzzyWordCompleter -from .nested import NestedCompleter -from .word_completer import WordCompleter - -__all__ = [ - # Base. - "Completion", - "Completer", - "ThreadedCompleter", - "DummyCompleter", - "DynamicCompleter", - "CompleteEvent", - "ConditionalCompleter", - "merge_completers", - "get_common_complete_suffix", - # Filesystem. - "PathCompleter", - "ExecutableCompleter", - # Fuzzy - "FuzzyCompleter", - "FuzzyWordCompleter", - # Nested. - "NestedCompleter", - # Word completer. - "WordCompleter", - # Deduplicate - "DeduplicateCompleter", -] +from .base import ( + CompleteEvent, + Completer, + Completion, + ConditionalCompleter, + DummyCompleter, + DynamicCompleter, + ThreadedCompleter, + get_common_complete_suffix, + merge_completers, +) +from .deduplicate import DeduplicateCompleter +from .filesystem import ExecutableCompleter, PathCompleter +from .fuzzy_completer import FuzzyCompleter, FuzzyWordCompleter +from .nested import NestedCompleter +from .word_completer import WordCompleter + +__all__ = [ + # Base. + "Completion", + "Completer", + "ThreadedCompleter", + "DummyCompleter", + "DynamicCompleter", + "CompleteEvent", + "ConditionalCompleter", + "merge_completers", + "get_common_complete_suffix", + # Filesystem. + "PathCompleter", + "ExecutableCompleter", + # Fuzzy + "FuzzyCompleter", + "FuzzyWordCompleter", + # Nested. + "NestedCompleter", + # Word completer. + "WordCompleter", + # Deduplicate + "DeduplicateCompleter", +] diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/base.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/base.py index a543b46c07..3bfde97d10 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/base.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/base.py @@ -1,398 +1,398 @@ -""" -""" -from abc import ABCMeta, abstractmethod -from typing import AsyncGenerator, Callable, Iterable, Optional, Sequence - -from prompt_toolkit.document import Document -from prompt_toolkit.eventloop import generator_to_async_generator -from prompt_toolkit.filters import FilterOrBool, to_filter -from prompt_toolkit.formatted_text import AnyFormattedText, StyleAndTextTuples - -__all__ = [ - "Completion", - "Completer", - "ThreadedCompleter", - "DummyCompleter", - "DynamicCompleter", - "CompleteEvent", - "ConditionalCompleter", - "merge_completers", - "get_common_complete_suffix", -] - - -class Completion: - """ - :param text: The new string that will be inserted into the document. - :param start_position: Position relative to the cursor_position where the - new text will start. The text will be inserted between the - start_position and the original cursor position. - :param display: (optional string or formatted text) If the completion has - to be displayed differently in the completion menu. - :param display_meta: (Optional string or formatted text) Meta information - about the completion, e.g. the path or source where it's coming from. - This can also be a callable that returns a string. - :param style: Style string. - :param selected_style: Style string, used for a selected completion. - This can override the `style` parameter. - """ - - def __init__( - self, - text: str, - start_position: int = 0, - display: Optional[AnyFormattedText] = None, - display_meta: Optional[AnyFormattedText] = None, - style: str = "", - selected_style: str = "", - ) -> None: - - from prompt_toolkit.formatted_text import to_formatted_text - - self.text = text - self.start_position = start_position - self._display_meta = display_meta - - if display is None: - display = text - - self.display = to_formatted_text(display) - - self.style = style - self.selected_style = selected_style - - assert self.start_position <= 0 - - def __repr__(self) -> str: - if isinstance(self.display, str) and self.display == self.text: - return "%s(text=%r, start_position=%r)" % ( - self.__class__.__name__, - self.text, - self.start_position, - ) - else: - return "%s(text=%r, start_position=%r, display=%r)" % ( - self.__class__.__name__, - self.text, - self.start_position, - self.display, - ) - - def __eq__(self, other: object) -> bool: - if not isinstance(other, Completion): - return False - return ( - self.text == other.text - and self.start_position == other.start_position - and self.display == other.display - and self._display_meta == other._display_meta - ) - - def __hash__(self) -> int: - return hash((self.text, self.start_position, self.display, self._display_meta)) - - @property - def display_text(self) -> str: - "The 'display' field as plain text." - from prompt_toolkit.formatted_text import fragment_list_to_text - - return fragment_list_to_text(self.display) - - @property - def display_meta(self) -> StyleAndTextTuples: - "Return meta-text. (This is lazy when using a callable)." - from prompt_toolkit.formatted_text import to_formatted_text - - return to_formatted_text(self._display_meta or "") - - @property - def display_meta_text(self) -> str: - "The 'meta' field as plain text." - from prompt_toolkit.formatted_text import fragment_list_to_text - - return fragment_list_to_text(self.display_meta) - - def new_completion_from_position(self, position: int) -> "Completion": - """ - (Only for internal use!) - Get a new completion by splitting this one. Used by `Application` when - it needs to have a list of new completions after inserting the common - prefix. - """ - assert position - self.start_position >= 0 - - return Completion( - text=self.text[position - self.start_position :], - display=self.display, - display_meta=self._display_meta, - ) - - -class CompleteEvent: - """ - Event that called the completer. - - :param text_inserted: When True, it means that completions are requested - because of a text insert. (`Buffer.complete_while_typing`.) - :param completion_requested: When True, it means that the user explicitly - pressed the `Tab` key in order to view the completions. - - These two flags can be used for instance to implement a completer that - shows some completions when ``Tab`` has been pressed, but not - automatically when the user presses a space. (Because of - `complete_while_typing`.) - """ - - def __init__( - self, text_inserted: bool = False, completion_requested: bool = False - ) -> None: - assert not (text_inserted and completion_requested) - - #: Automatic completion while typing. - self.text_inserted = text_inserted - - #: Used explicitly requested completion by pressing 'tab'. - self.completion_requested = completion_requested - - def __repr__(self) -> str: - return "%s(text_inserted=%r, completion_requested=%r)" % ( - self.__class__.__name__, - self.text_inserted, - self.completion_requested, - ) - - -class Completer(metaclass=ABCMeta): - """ - Base class for completer implementations. - """ - - @abstractmethod - def get_completions( - self, document: Document, complete_event: CompleteEvent - ) -> Iterable[Completion]: - """ - This should be a generator that yields :class:`.Completion` instances. - - If the generation of completions is something expensive (that takes a - lot of time), consider wrapping this `Completer` class in a - `ThreadedCompleter`. In that case, the completer algorithm runs in a - background thread and completions will be displayed as soon as they - arrive. - - :param document: :class:`~prompt_toolkit.document.Document` instance. - :param complete_event: :class:`.CompleteEvent` instance. - """ - while False: - yield - - async def get_completions_async( - self, document: Document, complete_event: CompleteEvent - ) -> AsyncGenerator[Completion, None]: - """ - Asynchronous generator for completions. (Probably, you won't have to - override this.) - - Asynchronous generator of :class:`.Completion` objects. - """ - for item in self.get_completions(document, complete_event): - yield item - - -class ThreadedCompleter(Completer): - """ - Wrapper that runs the `get_completions` generator in a thread. - - (Use this to prevent the user interface from becoming unresponsive if the - generation of completions takes too much time.) - - The completions will be displayed as soon as they are produced. The user - can already select a completion, even if not all completions are displayed. - """ - - def __init__(self, completer: Completer) -> None: - self.completer = completer - - def get_completions( - self, document: Document, complete_event: CompleteEvent - ) -> Iterable[Completion]: - return self.completer.get_completions(document, complete_event) - - async def get_completions_async( - self, document: Document, complete_event: CompleteEvent - ) -> AsyncGenerator[Completion, None]: - """ - Asynchronous generator of completions. - """ - async for completion in generator_to_async_generator( - lambda: self.completer.get_completions(document, complete_event) - ): - yield completion - - def __repr__(self) -> str: - return "ThreadedCompleter(%r)" % (self.completer,) - - -class DummyCompleter(Completer): - """ - A completer that doesn't return any completion. - """ - - def get_completions( - self, document: Document, complete_event: CompleteEvent - ) -> Iterable[Completion]: - return [] - - def __repr__(self) -> str: - return "DummyCompleter()" - - -class DynamicCompleter(Completer): - """ - Completer class that can dynamically returns any Completer. - - :param get_completer: Callable that returns a :class:`.Completer` instance. - """ - - def __init__(self, get_completer: Callable[[], Optional[Completer]]) -> None: - self.get_completer = get_completer - - def get_completions( - self, document: Document, complete_event: CompleteEvent - ) -> Iterable[Completion]: - completer = self.get_completer() or DummyCompleter() - return completer.get_completions(document, complete_event) - - async def get_completions_async( - self, document: Document, complete_event: CompleteEvent - ) -> AsyncGenerator[Completion, None]: - completer = self.get_completer() or DummyCompleter() - - async for completion in completer.get_completions_async( - document, complete_event - ): - yield completion - - def __repr__(self) -> str: - return "DynamicCompleter(%r -> %r)" % (self.get_completer, self.get_completer()) - - -class ConditionalCompleter(Completer): - """ - Wrapper around any other completer that will enable/disable the completions - depending on whether the received condition is satisfied. - - :param completer: :class:`.Completer` instance. - :param filter: :class:`.Filter` instance. - """ - - def __init__(self, completer: Completer, filter: FilterOrBool) -> None: - self.completer = completer - self.filter = to_filter(filter) - - def __repr__(self) -> str: - return "ConditionalCompleter(%r, filter=%r)" % (self.completer, self.filter) - - def get_completions( - self, document: Document, complete_event: CompleteEvent - ) -> Iterable[Completion]: - # Get all completions in a blocking way. - if self.filter(): - for c in self.completer.get_completions(document, complete_event): - yield c - - async def get_completions_async( - self, document: Document, complete_event: CompleteEvent - ) -> AsyncGenerator[Completion, None]: - - # Get all completions in a non-blocking way. - if self.filter(): - async for item in self.completer.get_completions_async( - document, complete_event - ): - yield item - - -class _MergedCompleter(Completer): - """ - Combine several completers into one. - """ - - def __init__(self, completers: Sequence[Completer]) -> None: - self.completers = completers - - def get_completions( - self, document: Document, complete_event: CompleteEvent - ) -> Iterable[Completion]: - # Get all completions from the other completers in a blocking way. - for completer in self.completers: - for c in completer.get_completions(document, complete_event): - yield c - - async def get_completions_async( - self, document: Document, complete_event: CompleteEvent - ) -> AsyncGenerator[Completion, None]: - - # Get all completions from the other completers in a non-blocking way. - for completer in self.completers: - async for item in completer.get_completions_async(document, complete_event): - yield item - - -def merge_completers( - completers: Sequence[Completer], deduplicate: bool = False -) -> Completer: - """ - Combine several completers into one. - - :param deduplicate: If `True`, wrap the result in a `DeduplicateCompleter` - so that completions that would result in the same text will be - deduplicated. - """ - if deduplicate: - from .deduplicate import DeduplicateCompleter - - return DeduplicateCompleter(_MergedCompleter(completers)) - - return _MergedCompleter(completers) - - -def get_common_complete_suffix( - document: Document, completions: Sequence[Completion] -) -> str: - """ - Return the common prefix for all completions. - """ - # Take only completions that don't change the text before the cursor. - def doesnt_change_before_cursor(completion: Completion) -> bool: - end = completion.text[: -completion.start_position] - return document.text_before_cursor.endswith(end) - - completions2 = [c for c in completions if doesnt_change_before_cursor(c)] - - # When there is at least one completion that changes the text before the - # cursor, don't return any common part. - if len(completions2) != len(completions): - return "" - - # Return the common prefix. - def get_suffix(completion: Completion) -> str: - return completion.text[-completion.start_position :] - - return _commonprefix([get_suffix(c) for c in completions2]) - - -def _commonprefix(strings: Iterable[str]) -> str: - # Similar to os.path.commonprefix - if not strings: - return "" - - else: - s1 = min(strings) - s2 = max(strings) - - for i, c in enumerate(s1): - if c != s2[i]: - return s1[:i] - - return s1 +""" +""" +from abc import ABCMeta, abstractmethod +from typing import AsyncGenerator, Callable, Iterable, Optional, Sequence + +from prompt_toolkit.document import Document +from prompt_toolkit.eventloop import generator_to_async_generator +from prompt_toolkit.filters import FilterOrBool, to_filter +from prompt_toolkit.formatted_text import AnyFormattedText, StyleAndTextTuples + +__all__ = [ + "Completion", + "Completer", + "ThreadedCompleter", + "DummyCompleter", + "DynamicCompleter", + "CompleteEvent", + "ConditionalCompleter", + "merge_completers", + "get_common_complete_suffix", +] + + +class Completion: + """ + :param text: The new string that will be inserted into the document. + :param start_position: Position relative to the cursor_position where the + new text will start. The text will be inserted between the + start_position and the original cursor position. + :param display: (optional string or formatted text) If the completion has + to be displayed differently in the completion menu. + :param display_meta: (Optional string or formatted text) Meta information + about the completion, e.g. the path or source where it's coming from. + This can also be a callable that returns a string. + :param style: Style string. + :param selected_style: Style string, used for a selected completion. + This can override the `style` parameter. + """ + + def __init__( + self, + text: str, + start_position: int = 0, + display: Optional[AnyFormattedText] = None, + display_meta: Optional[AnyFormattedText] = None, + style: str = "", + selected_style: str = "", + ) -> None: + + from prompt_toolkit.formatted_text import to_formatted_text + + self.text = text + self.start_position = start_position + self._display_meta = display_meta + + if display is None: + display = text + + self.display = to_formatted_text(display) + + self.style = style + self.selected_style = selected_style + + assert self.start_position <= 0 + + def __repr__(self) -> str: + if isinstance(self.display, str) and self.display == self.text: + return "%s(text=%r, start_position=%r)" % ( + self.__class__.__name__, + self.text, + self.start_position, + ) + else: + return "%s(text=%r, start_position=%r, display=%r)" % ( + self.__class__.__name__, + self.text, + self.start_position, + self.display, + ) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Completion): + return False + return ( + self.text == other.text + and self.start_position == other.start_position + and self.display == other.display + and self._display_meta == other._display_meta + ) + + def __hash__(self) -> int: + return hash((self.text, self.start_position, self.display, self._display_meta)) + + @property + def display_text(self) -> str: + "The 'display' field as plain text." + from prompt_toolkit.formatted_text import fragment_list_to_text + + return fragment_list_to_text(self.display) + + @property + def display_meta(self) -> StyleAndTextTuples: + "Return meta-text. (This is lazy when using a callable)." + from prompt_toolkit.formatted_text import to_formatted_text + + return to_formatted_text(self._display_meta or "") + + @property + def display_meta_text(self) -> str: + "The 'meta' field as plain text." + from prompt_toolkit.formatted_text import fragment_list_to_text + + return fragment_list_to_text(self.display_meta) + + def new_completion_from_position(self, position: int) -> "Completion": + """ + (Only for internal use!) + Get a new completion by splitting this one. Used by `Application` when + it needs to have a list of new completions after inserting the common + prefix. + """ + assert position - self.start_position >= 0 + + return Completion( + text=self.text[position - self.start_position :], + display=self.display, + display_meta=self._display_meta, + ) + + +class CompleteEvent: + """ + Event that called the completer. + + :param text_inserted: When True, it means that completions are requested + because of a text insert. (`Buffer.complete_while_typing`.) + :param completion_requested: When True, it means that the user explicitly + pressed the `Tab` key in order to view the completions. + + These two flags can be used for instance to implement a completer that + shows some completions when ``Tab`` has been pressed, but not + automatically when the user presses a space. (Because of + `complete_while_typing`.) + """ + + def __init__( + self, text_inserted: bool = False, completion_requested: bool = False + ) -> None: + assert not (text_inserted and completion_requested) + + #: Automatic completion while typing. + self.text_inserted = text_inserted + + #: Used explicitly requested completion by pressing 'tab'. + self.completion_requested = completion_requested + + def __repr__(self) -> str: + return "%s(text_inserted=%r, completion_requested=%r)" % ( + self.__class__.__name__, + self.text_inserted, + self.completion_requested, + ) + + +class Completer(metaclass=ABCMeta): + """ + Base class for completer implementations. + """ + + @abstractmethod + def get_completions( + self, document: Document, complete_event: CompleteEvent + ) -> Iterable[Completion]: + """ + This should be a generator that yields :class:`.Completion` instances. + + If the generation of completions is something expensive (that takes a + lot of time), consider wrapping this `Completer` class in a + `ThreadedCompleter`. In that case, the completer algorithm runs in a + background thread and completions will be displayed as soon as they + arrive. + + :param document: :class:`~prompt_toolkit.document.Document` instance. + :param complete_event: :class:`.CompleteEvent` instance. + """ + while False: + yield + + async def get_completions_async( + self, document: Document, complete_event: CompleteEvent + ) -> AsyncGenerator[Completion, None]: + """ + Asynchronous generator for completions. (Probably, you won't have to + override this.) + + Asynchronous generator of :class:`.Completion` objects. + """ + for item in self.get_completions(document, complete_event): + yield item + + +class ThreadedCompleter(Completer): + """ + Wrapper that runs the `get_completions` generator in a thread. + + (Use this to prevent the user interface from becoming unresponsive if the + generation of completions takes too much time.) + + The completions will be displayed as soon as they are produced. The user + can already select a completion, even if not all completions are displayed. + """ + + def __init__(self, completer: Completer) -> None: + self.completer = completer + + def get_completions( + self, document: Document, complete_event: CompleteEvent + ) -> Iterable[Completion]: + return self.completer.get_completions(document, complete_event) + + async def get_completions_async( + self, document: Document, complete_event: CompleteEvent + ) -> AsyncGenerator[Completion, None]: + """ + Asynchronous generator of completions. + """ + async for completion in generator_to_async_generator( + lambda: self.completer.get_completions(document, complete_event) + ): + yield completion + + def __repr__(self) -> str: + return "ThreadedCompleter(%r)" % (self.completer,) + + +class DummyCompleter(Completer): + """ + A completer that doesn't return any completion. + """ + + def get_completions( + self, document: Document, complete_event: CompleteEvent + ) -> Iterable[Completion]: + return [] + + def __repr__(self) -> str: + return "DummyCompleter()" + + +class DynamicCompleter(Completer): + """ + Completer class that can dynamically returns any Completer. + + :param get_completer: Callable that returns a :class:`.Completer` instance. + """ + + def __init__(self, get_completer: Callable[[], Optional[Completer]]) -> None: + self.get_completer = get_completer + + def get_completions( + self, document: Document, complete_event: CompleteEvent + ) -> Iterable[Completion]: + completer = self.get_completer() or DummyCompleter() + return completer.get_completions(document, complete_event) + + async def get_completions_async( + self, document: Document, complete_event: CompleteEvent + ) -> AsyncGenerator[Completion, None]: + completer = self.get_completer() or DummyCompleter() + + async for completion in completer.get_completions_async( + document, complete_event + ): + yield completion + + def __repr__(self) -> str: + return "DynamicCompleter(%r -> %r)" % (self.get_completer, self.get_completer()) + + +class ConditionalCompleter(Completer): + """ + Wrapper around any other completer that will enable/disable the completions + depending on whether the received condition is satisfied. + + :param completer: :class:`.Completer` instance. + :param filter: :class:`.Filter` instance. + """ + + def __init__(self, completer: Completer, filter: FilterOrBool) -> None: + self.completer = completer + self.filter = to_filter(filter) + + def __repr__(self) -> str: + return "ConditionalCompleter(%r, filter=%r)" % (self.completer, self.filter) + + def get_completions( + self, document: Document, complete_event: CompleteEvent + ) -> Iterable[Completion]: + # Get all completions in a blocking way. + if self.filter(): + for c in self.completer.get_completions(document, complete_event): + yield c + + async def get_completions_async( + self, document: Document, complete_event: CompleteEvent + ) -> AsyncGenerator[Completion, None]: + + # Get all completions in a non-blocking way. + if self.filter(): + async for item in self.completer.get_completions_async( + document, complete_event + ): + yield item + + +class _MergedCompleter(Completer): + """ + Combine several completers into one. + """ + + def __init__(self, completers: Sequence[Completer]) -> None: + self.completers = completers + + def get_completions( + self, document: Document, complete_event: CompleteEvent + ) -> Iterable[Completion]: + # Get all completions from the other completers in a blocking way. + for completer in self.completers: + for c in completer.get_completions(document, complete_event): + yield c + + async def get_completions_async( + self, document: Document, complete_event: CompleteEvent + ) -> AsyncGenerator[Completion, None]: + + # Get all completions from the other completers in a non-blocking way. + for completer in self.completers: + async for item in completer.get_completions_async(document, complete_event): + yield item + + +def merge_completers( + completers: Sequence[Completer], deduplicate: bool = False +) -> Completer: + """ + Combine several completers into one. + + :param deduplicate: If `True`, wrap the result in a `DeduplicateCompleter` + so that completions that would result in the same text will be + deduplicated. + """ + if deduplicate: + from .deduplicate import DeduplicateCompleter + + return DeduplicateCompleter(_MergedCompleter(completers)) + + return _MergedCompleter(completers) + + +def get_common_complete_suffix( + document: Document, completions: Sequence[Completion] +) -> str: + """ + Return the common prefix for all completions. + """ + # Take only completions that don't change the text before the cursor. + def doesnt_change_before_cursor(completion: Completion) -> bool: + end = completion.text[: -completion.start_position] + return document.text_before_cursor.endswith(end) + + completions2 = [c for c in completions if doesnt_change_before_cursor(c)] + + # When there is at least one completion that changes the text before the + # cursor, don't return any common part. + if len(completions2) != len(completions): + return "" + + # Return the common prefix. + def get_suffix(completion: Completion) -> str: + return completion.text[-completion.start_position :] + + return _commonprefix([get_suffix(c) for c in completions2]) + + +def _commonprefix(strings: Iterable[str]) -> str: + # Similar to os.path.commonprefix + if not strings: + return "" + + else: + s1 = min(strings) + s2 = max(strings) + + for i, c in enumerate(s1): + if c != s2[i]: + return s1[:i] + + return s1 diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/deduplicate.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/deduplicate.py index b717d90bb0..6ef95224a6 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/deduplicate.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/deduplicate.py @@ -1,43 +1,43 @@ -from typing import Iterable, Set - -from prompt_toolkit.document import Document - -from .base import CompleteEvent, Completer, Completion - -__all__ = ["DeduplicateCompleter"] - - -class DeduplicateCompleter(Completer): - """ - Wrapper around a completer that removes duplicates. Only the first unique - completions are kept. - - Completions are considered to be a duplicate if they result in the same - document text when they would be applied. - """ - - def __init__(self, completer: Completer) -> None: - self.completer = completer - - def get_completions( - self, document: Document, complete_event: CompleteEvent - ) -> Iterable[Completion]: - # Keep track of the document strings we'd get after applying any completion. - found_so_far: Set[str] = set() - - for completion in self.completer.get_completions(document, complete_event): - text_if_applied = ( - document.text[: document.cursor_position + completion.start_position] - + completion.text - + document.text[document.cursor_position :] - ) - - if text_if_applied == document.text: - # Don't include completions that don't have any effect at all. - continue - - if text_if_applied in found_so_far: - continue - - found_so_far.add(text_if_applied) - yield completion +from typing import Iterable, Set + +from prompt_toolkit.document import Document + +from .base import CompleteEvent, Completer, Completion + +__all__ = ["DeduplicateCompleter"] + + +class DeduplicateCompleter(Completer): + """ + Wrapper around a completer that removes duplicates. Only the first unique + completions are kept. + + Completions are considered to be a duplicate if they result in the same + document text when they would be applied. + """ + + def __init__(self, completer: Completer) -> None: + self.completer = completer + + def get_completions( + self, document: Document, complete_event: CompleteEvent + ) -> Iterable[Completion]: + # Keep track of the document strings we'd get after applying any completion. + found_so_far: Set[str] = set() + + for completion in self.completer.get_completions(document, complete_event): + text_if_applied = ( + document.text[: document.cursor_position + completion.start_position] + + completion.text + + document.text[document.cursor_position :] + ) + + if text_if_applied == document.text: + # Don't include completions that don't have any effect at all. + continue + + if text_if_applied in found_so_far: + continue + + found_so_far.add(text_if_applied) + yield completion diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/filesystem.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/filesystem.py index 232cecb863..719eac6097 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/filesystem.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/filesystem.py @@ -1,117 +1,117 @@ -import os -from typing import Callable, Iterable, List, Optional - -from prompt_toolkit.completion import CompleteEvent, Completer, Completion -from prompt_toolkit.document import Document - -__all__ = [ - "PathCompleter", - "ExecutableCompleter", -] - - -class PathCompleter(Completer): - """ - Complete for Path variables. - - :param get_paths: Callable which returns a list of directories to look into - when the user enters a relative path. - :param file_filter: Callable which takes a filename and returns whether - this file should show up in the completion. ``None`` - when no filtering has to be done. - :param min_input_len: Don't do autocompletion when the input string is shorter. - """ - - def __init__( - self, - only_directories: bool = False, - get_paths: Optional[Callable[[], List[str]]] = None, - file_filter: Optional[Callable[[str], bool]] = None, - min_input_len: int = 0, - expanduser: bool = False, - ) -> None: - - self.only_directories = only_directories - self.get_paths = get_paths or (lambda: ["."]) - self.file_filter = file_filter or (lambda _: True) - self.min_input_len = min_input_len - self.expanduser = expanduser - - def get_completions( - self, document: Document, complete_event: CompleteEvent - ) -> Iterable[Completion]: - text = document.text_before_cursor - - # Complete only when we have at least the minimal input length, - # otherwise, we can too many results and autocompletion will become too - # heavy. - if len(text) < self.min_input_len: - return - - try: - # Do tilde expansion. - if self.expanduser: - text = os.path.expanduser(text) - - # Directories where to look. - dirname = os.path.dirname(text) - if dirname: - directories = [ - os.path.dirname(os.path.join(p, text)) for p in self.get_paths() - ] - else: - directories = self.get_paths() - - # Start of current file. - prefix = os.path.basename(text) - - # Get all filenames. - filenames = [] - for directory in directories: - # Look for matches in this directory. - if os.path.isdir(directory): - for filename in os.listdir(directory): - if filename.startswith(prefix): - filenames.append((directory, filename)) - - # Sort - filenames = sorted(filenames, key=lambda k: k[1]) - - # Yield them. - for directory, filename in filenames: - completion = filename[len(prefix) :] - full_name = os.path.join(directory, filename) - - if os.path.isdir(full_name): - # For directories, add a slash to the filename. - # (We don't add them to the `completion`. Users can type it - # to trigger the autocompletion themselves.) - filename += "/" - elif self.only_directories: - continue - - if not self.file_filter(full_name): - continue - +import os +from typing import Callable, Iterable, List, Optional + +from prompt_toolkit.completion import CompleteEvent, Completer, Completion +from prompt_toolkit.document import Document + +__all__ = [ + "PathCompleter", + "ExecutableCompleter", +] + + +class PathCompleter(Completer): + """ + Complete for Path variables. + + :param get_paths: Callable which returns a list of directories to look into + when the user enters a relative path. + :param file_filter: Callable which takes a filename and returns whether + this file should show up in the completion. ``None`` + when no filtering has to be done. + :param min_input_len: Don't do autocompletion when the input string is shorter. + """ + + def __init__( + self, + only_directories: bool = False, + get_paths: Optional[Callable[[], List[str]]] = None, + file_filter: Optional[Callable[[str], bool]] = None, + min_input_len: int = 0, + expanduser: bool = False, + ) -> None: + + self.only_directories = only_directories + self.get_paths = get_paths or (lambda: ["."]) + self.file_filter = file_filter or (lambda _: True) + self.min_input_len = min_input_len + self.expanduser = expanduser + + def get_completions( + self, document: Document, complete_event: CompleteEvent + ) -> Iterable[Completion]: + text = document.text_before_cursor + + # Complete only when we have at least the minimal input length, + # otherwise, we can too many results and autocompletion will become too + # heavy. + if len(text) < self.min_input_len: + return + + try: + # Do tilde expansion. + if self.expanduser: + text = os.path.expanduser(text) + + # Directories where to look. + dirname = os.path.dirname(text) + if dirname: + directories = [ + os.path.dirname(os.path.join(p, text)) for p in self.get_paths() + ] + else: + directories = self.get_paths() + + # Start of current file. + prefix = os.path.basename(text) + + # Get all filenames. + filenames = [] + for directory in directories: + # Look for matches in this directory. + if os.path.isdir(directory): + for filename in os.listdir(directory): + if filename.startswith(prefix): + filenames.append((directory, filename)) + + # Sort + filenames = sorted(filenames, key=lambda k: k[1]) + + # Yield them. + for directory, filename in filenames: + completion = filename[len(prefix) :] + full_name = os.path.join(directory, filename) + + if os.path.isdir(full_name): + # For directories, add a slash to the filename. + # (We don't add them to the `completion`. Users can type it + # to trigger the autocompletion themselves.) + filename += "/" + elif self.only_directories: + continue + + if not self.file_filter(full_name): + continue + yield Completion( text=completion, start_position=0, display=filename, ) - except OSError: - pass - - -class ExecutableCompleter(PathCompleter): - """ - Complete only executable files in the current path. - """ - - def __init__(self) -> None: - super().__init__( - only_directories=False, - min_input_len=1, - get_paths=lambda: os.environ.get("PATH", "").split(os.pathsep), - file_filter=lambda name: os.access(name, os.X_OK), - expanduser=True, - ), + except OSError: + pass + + +class ExecutableCompleter(PathCompleter): + """ + Complete only executable files in the current path. + """ + + def __init__(self) -> None: + super().__init__( + only_directories=False, + min_input_len=1, + get_paths=lambda: os.environ.get("PATH", "").split(os.pathsep), + file_filter=lambda name: os.access(name, os.X_OK), + expanduser=True, + ), diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/fuzzy_completer.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/fuzzy_completer.py index 8f9be0f028..4f7c3ab5d6 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/fuzzy_completer.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/fuzzy_completer.py @@ -1,201 +1,201 @@ -import re -from typing import Callable, Dict, Iterable, List, NamedTuple, Optional, Tuple, Union - -from prompt_toolkit.document import Document -from prompt_toolkit.filters import FilterOrBool, to_filter -from prompt_toolkit.formatted_text import AnyFormattedText, StyleAndTextTuples - -from .base import CompleteEvent, Completer, Completion -from .word_completer import WordCompleter - -__all__ = [ - "FuzzyCompleter", - "FuzzyWordCompleter", -] - - -class FuzzyCompleter(Completer): - """ - Fuzzy completion. - This wraps any other completer and turns it into a fuzzy completer. - - If the list of words is: ["leopard" , "gorilla", "dinosaur", "cat", "bee"] - Then trying to complete "oar" would yield "leopard" and "dinosaur", but not - the others, because they match the regular expression 'o.*a.*r'. - Similar, in another application "djm" could expand to "django_migrations". - - The results are sorted by relevance, which is defined as the start position - and the length of the match. - - Notice that this is not really a tool to work around spelling mistakes, - like what would be possible with difflib. The purpose is rather to have a - quicker or more intuitive way to filter the given completions, especially - when many completions have a common prefix. - - Fuzzy algorithm is based on this post: - https://blog.amjith.com/fuzzyfinder-in-10-lines-of-python - - :param completer: A :class:`~.Completer` instance. - :param WORD: When True, use WORD characters. - :param pattern: Regex pattern which selects the characters before the - cursor that are considered for the fuzzy matching. - :param enable_fuzzy: (bool or `Filter`) Enabled the fuzzy behavior. For - easily turning fuzzyness on or off according to a certain condition. - """ - - def __init__( - self, - completer: Completer, - WORD: bool = False, - pattern: Optional[str] = None, - enable_fuzzy: FilterOrBool = True, - ): - - assert pattern is None or pattern.startswith("^") - - self.completer = completer - self.pattern = pattern - self.WORD = WORD - self.pattern = pattern - self.enable_fuzzy = to_filter(enable_fuzzy) - - def get_completions( - self, document: Document, complete_event: CompleteEvent - ) -> Iterable[Completion]: - if self.enable_fuzzy(): - return self._get_fuzzy_completions(document, complete_event) - else: - return self.completer.get_completions(document, complete_event) - - def _get_pattern(self) -> str: - if self.pattern: - return self.pattern - if self.WORD: - return r"[^\s]+" - return "^[a-zA-Z0-9_]*" - - def _get_fuzzy_completions( - self, document: Document, complete_event: CompleteEvent - ) -> Iterable[Completion]: - - word_before_cursor = document.get_word_before_cursor( - pattern=re.compile(self._get_pattern()) - ) - - # Get completions - document2 = Document( - text=document.text[: document.cursor_position - len(word_before_cursor)], - cursor_position=document.cursor_position - len(word_before_cursor), - ) - - completions = list(self.completer.get_completions(document2, complete_event)) - - fuzzy_matches: List[_FuzzyMatch] = [] - - pat = ".*?".join(map(re.escape, word_before_cursor)) - pat = "(?=({0}))".format(pat) # lookahead regex to manage overlapping matches - regex = re.compile(pat, re.IGNORECASE) - for compl in completions: - matches = list(regex.finditer(compl.text)) - if matches: - # Prefer the match, closest to the left, then shortest. - best = min(matches, key=lambda m: (m.start(), len(m.group(1)))) - fuzzy_matches.append( - _FuzzyMatch(len(best.group(1)), best.start(), compl) - ) - - def sort_key(fuzzy_match: "_FuzzyMatch") -> Tuple[int, int]: - "Sort by start position, then by the length of the match." - return fuzzy_match.start_pos, fuzzy_match.match_length - - fuzzy_matches = sorted(fuzzy_matches, key=sort_key) - - for match in fuzzy_matches: - # Include these completions, but set the correct `display` - # attribute and `start_position`. - yield Completion( +import re +from typing import Callable, Dict, Iterable, List, NamedTuple, Optional, Tuple, Union + +from prompt_toolkit.document import Document +from prompt_toolkit.filters import FilterOrBool, to_filter +from prompt_toolkit.formatted_text import AnyFormattedText, StyleAndTextTuples + +from .base import CompleteEvent, Completer, Completion +from .word_completer import WordCompleter + +__all__ = [ + "FuzzyCompleter", + "FuzzyWordCompleter", +] + + +class FuzzyCompleter(Completer): + """ + Fuzzy completion. + This wraps any other completer and turns it into a fuzzy completer. + + If the list of words is: ["leopard" , "gorilla", "dinosaur", "cat", "bee"] + Then trying to complete "oar" would yield "leopard" and "dinosaur", but not + the others, because they match the regular expression 'o.*a.*r'. + Similar, in another application "djm" could expand to "django_migrations". + + The results are sorted by relevance, which is defined as the start position + and the length of the match. + + Notice that this is not really a tool to work around spelling mistakes, + like what would be possible with difflib. The purpose is rather to have a + quicker or more intuitive way to filter the given completions, especially + when many completions have a common prefix. + + Fuzzy algorithm is based on this post: + https://blog.amjith.com/fuzzyfinder-in-10-lines-of-python + + :param completer: A :class:`~.Completer` instance. + :param WORD: When True, use WORD characters. + :param pattern: Regex pattern which selects the characters before the + cursor that are considered for the fuzzy matching. + :param enable_fuzzy: (bool or `Filter`) Enabled the fuzzy behavior. For + easily turning fuzzyness on or off according to a certain condition. + """ + + def __init__( + self, + completer: Completer, + WORD: bool = False, + pattern: Optional[str] = None, + enable_fuzzy: FilterOrBool = True, + ): + + assert pattern is None or pattern.startswith("^") + + self.completer = completer + self.pattern = pattern + self.WORD = WORD + self.pattern = pattern + self.enable_fuzzy = to_filter(enable_fuzzy) + + def get_completions( + self, document: Document, complete_event: CompleteEvent + ) -> Iterable[Completion]: + if self.enable_fuzzy(): + return self._get_fuzzy_completions(document, complete_event) + else: + return self.completer.get_completions(document, complete_event) + + def _get_pattern(self) -> str: + if self.pattern: + return self.pattern + if self.WORD: + return r"[^\s]+" + return "^[a-zA-Z0-9_]*" + + def _get_fuzzy_completions( + self, document: Document, complete_event: CompleteEvent + ) -> Iterable[Completion]: + + word_before_cursor = document.get_word_before_cursor( + pattern=re.compile(self._get_pattern()) + ) + + # Get completions + document2 = Document( + text=document.text[: document.cursor_position - len(word_before_cursor)], + cursor_position=document.cursor_position - len(word_before_cursor), + ) + + completions = list(self.completer.get_completions(document2, complete_event)) + + fuzzy_matches: List[_FuzzyMatch] = [] + + pat = ".*?".join(map(re.escape, word_before_cursor)) + pat = "(?=({0}))".format(pat) # lookahead regex to manage overlapping matches + regex = re.compile(pat, re.IGNORECASE) + for compl in completions: + matches = list(regex.finditer(compl.text)) + if matches: + # Prefer the match, closest to the left, then shortest. + best = min(matches, key=lambda m: (m.start(), len(m.group(1)))) + fuzzy_matches.append( + _FuzzyMatch(len(best.group(1)), best.start(), compl) + ) + + def sort_key(fuzzy_match: "_FuzzyMatch") -> Tuple[int, int]: + "Sort by start position, then by the length of the match." + return fuzzy_match.start_pos, fuzzy_match.match_length + + fuzzy_matches = sorted(fuzzy_matches, key=sort_key) + + for match in fuzzy_matches: + # Include these completions, but set the correct `display` + # attribute and `start_position`. + yield Completion( text=match.completion.text, - start_position=match.completion.start_position - - len(word_before_cursor), - display_meta=match.completion.display_meta, - display=self._get_display(match, word_before_cursor), - style=match.completion.style, - ) - - def _get_display( - self, fuzzy_match: "_FuzzyMatch", word_before_cursor: str - ) -> AnyFormattedText: - """ - Generate formatted text for the display label. - """ - m = fuzzy_match - word = m.completion.text - - if m.match_length == 0: - # No highlighting when we have zero length matches (no input text). - # In this case, use the original display text (which can include - # additional styling or characters). - return m.completion.display - - result: StyleAndTextTuples = [] - - # Text before match. - result.append(("class:fuzzymatch.outside", word[: m.start_pos])) - - # The match itself. - characters = list(word_before_cursor) - - for c in word[m.start_pos : m.start_pos + m.match_length]: - classname = "class:fuzzymatch.inside" - if characters and c.lower() == characters[0].lower(): - classname += ".character" - del characters[0] - - result.append((classname, c)) - - # Text after match. - result.append( - ("class:fuzzymatch.outside", word[m.start_pos + m.match_length :]) - ) - - return result - - -class FuzzyWordCompleter(Completer): - """ - Fuzzy completion on a list of words. - - (This is basically a `WordCompleter` wrapped in a `FuzzyCompleter`.) - - :param words: List of words or callable that returns a list of words. - :param meta_dict: Optional dict mapping words to their meta-information. - :param WORD: When True, use WORD characters. - """ - - def __init__( - self, - words: Union[List[str], Callable[[], List[str]]], - meta_dict: Optional[Dict[str, str]] = None, - WORD: bool = False, - ) -> None: - - self.words = words - self.meta_dict = meta_dict or {} - self.WORD = WORD - - self.word_completer = WordCompleter( - words=self.words, WORD=self.WORD, meta_dict=self.meta_dict - ) - - self.fuzzy_completer = FuzzyCompleter(self.word_completer, WORD=self.WORD) - - def get_completions( - self, document: Document, complete_event: CompleteEvent - ) -> Iterable[Completion]: - return self.fuzzy_completer.get_completions(document, complete_event) - - -_FuzzyMatch = NamedTuple( - "_FuzzyMatch", - [("match_length", int), ("start_pos", int), ("completion", Completion)], -) + start_position=match.completion.start_position + - len(word_before_cursor), + display_meta=match.completion.display_meta, + display=self._get_display(match, word_before_cursor), + style=match.completion.style, + ) + + def _get_display( + self, fuzzy_match: "_FuzzyMatch", word_before_cursor: str + ) -> AnyFormattedText: + """ + Generate formatted text for the display label. + """ + m = fuzzy_match + word = m.completion.text + + if m.match_length == 0: + # No highlighting when we have zero length matches (no input text). + # In this case, use the original display text (which can include + # additional styling or characters). + return m.completion.display + + result: StyleAndTextTuples = [] + + # Text before match. + result.append(("class:fuzzymatch.outside", word[: m.start_pos])) + + # The match itself. + characters = list(word_before_cursor) + + for c in word[m.start_pos : m.start_pos + m.match_length]: + classname = "class:fuzzymatch.inside" + if characters and c.lower() == characters[0].lower(): + classname += ".character" + del characters[0] + + result.append((classname, c)) + + # Text after match. + result.append( + ("class:fuzzymatch.outside", word[m.start_pos + m.match_length :]) + ) + + return result + + +class FuzzyWordCompleter(Completer): + """ + Fuzzy completion on a list of words. + + (This is basically a `WordCompleter` wrapped in a `FuzzyCompleter`.) + + :param words: List of words or callable that returns a list of words. + :param meta_dict: Optional dict mapping words to their meta-information. + :param WORD: When True, use WORD characters. + """ + + def __init__( + self, + words: Union[List[str], Callable[[], List[str]]], + meta_dict: Optional[Dict[str, str]] = None, + WORD: bool = False, + ) -> None: + + self.words = words + self.meta_dict = meta_dict or {} + self.WORD = WORD + + self.word_completer = WordCompleter( + words=self.words, WORD=self.WORD, meta_dict=self.meta_dict + ) + + self.fuzzy_completer = FuzzyCompleter(self.word_completer, WORD=self.WORD) + + def get_completions( + self, document: Document, complete_event: CompleteEvent + ) -> Iterable[Completion]: + return self.fuzzy_completer.get_completions(document, complete_event) + + +_FuzzyMatch = NamedTuple( + "_FuzzyMatch", + [("match_length", int), ("start_pos", int), ("completion", Completion)], +) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/nested.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/nested.py index f469f41016..8b0978ca32 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/nested.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/nested.py @@ -1,109 +1,109 @@ -""" -Nestedcompleter for completion of hierarchical data structures. -""" -from typing import Any, Dict, Iterable, Mapping, Optional, Set, Union - -from prompt_toolkit.completion import CompleteEvent, Completer, Completion -from prompt_toolkit.completion.word_completer import WordCompleter -from prompt_toolkit.document import Document - -__all__ = ["NestedCompleter"] - -# NestedDict = Mapping[str, Union['NestedDict', Set[str], None, Completer]] -NestedDict = Mapping[str, Union[Any, Set[str], None, Completer]] - - -class NestedCompleter(Completer): - """ - Completer which wraps around several other completers, and calls any the - one that corresponds with the first word of the input. - - By combining multiple `NestedCompleter` instances, we can achieve multiple - hierarchical levels of autocompletion. This is useful when `WordCompleter` - is not sufficient. - - If you need multiple levels, check out the `from_nested_dict` classmethod. - """ - - def __init__( - self, options: Dict[str, Optional[Completer]], ignore_case: bool = True - ) -> None: - - self.options = options - self.ignore_case = ignore_case - - def __repr__(self) -> str: - return "NestedCompleter(%r, ignore_case=%r)" % (self.options, self.ignore_case) - - @classmethod - def from_nested_dict(cls, data: NestedDict) -> "NestedCompleter": - """ - Create a `NestedCompleter`, starting from a nested dictionary data - structure, like this: - - .. code:: - - data = { - 'show': { - 'version': None, - 'interfaces': None, - 'clock': None, - 'ip': {'interface': {'brief'}} - }, - 'exit': None - 'enable': None - } - - The value should be `None` if there is no further completion at some - point. If all values in the dictionary are None, it is also possible to - use a set instead. - - Values in this data structure can be a completers as well. - """ - options: Dict[str, Optional[Completer]] = {} - for key, value in data.items(): - if isinstance(value, Completer): - options[key] = value - elif isinstance(value, dict): - options[key] = cls.from_nested_dict(value) - elif isinstance(value, set): - options[key] = cls.from_nested_dict({item: None for item in value}) - else: - assert value is None - options[key] = None - - return cls(options) - - def get_completions( - self, document: Document, complete_event: CompleteEvent - ) -> Iterable[Completion]: - # Split document. - text = document.text_before_cursor.lstrip() - stripped_len = len(document.text_before_cursor) - len(text) - - # If there is a space, check for the first term, and use a - # subcompleter. - if " " in text: - first_term = text.split()[0] - completer = self.options.get(first_term) - - # If we have a sub completer, use this for the completions. - if completer is not None: - remaining_text = text[len(first_term) :].lstrip() - move_cursor = len(text) - len(remaining_text) + stripped_len - - new_document = Document( - remaining_text, - cursor_position=document.cursor_position - move_cursor, - ) - - for c in completer.get_completions(new_document, complete_event): - yield c - - # No space in the input: behave exactly like `WordCompleter`. - else: - completer = WordCompleter( - list(self.options.keys()), ignore_case=self.ignore_case - ) - for c in completer.get_completions(document, complete_event): - yield c +""" +Nestedcompleter for completion of hierarchical data structures. +""" +from typing import Any, Dict, Iterable, Mapping, Optional, Set, Union + +from prompt_toolkit.completion import CompleteEvent, Completer, Completion +from prompt_toolkit.completion.word_completer import WordCompleter +from prompt_toolkit.document import Document + +__all__ = ["NestedCompleter"] + +# NestedDict = Mapping[str, Union['NestedDict', Set[str], None, Completer]] +NestedDict = Mapping[str, Union[Any, Set[str], None, Completer]] + + +class NestedCompleter(Completer): + """ + Completer which wraps around several other completers, and calls any the + one that corresponds with the first word of the input. + + By combining multiple `NestedCompleter` instances, we can achieve multiple + hierarchical levels of autocompletion. This is useful when `WordCompleter` + is not sufficient. + + If you need multiple levels, check out the `from_nested_dict` classmethod. + """ + + def __init__( + self, options: Dict[str, Optional[Completer]], ignore_case: bool = True + ) -> None: + + self.options = options + self.ignore_case = ignore_case + + def __repr__(self) -> str: + return "NestedCompleter(%r, ignore_case=%r)" % (self.options, self.ignore_case) + + @classmethod + def from_nested_dict(cls, data: NestedDict) -> "NestedCompleter": + """ + Create a `NestedCompleter`, starting from a nested dictionary data + structure, like this: + + .. code:: + + data = { + 'show': { + 'version': None, + 'interfaces': None, + 'clock': None, + 'ip': {'interface': {'brief'}} + }, + 'exit': None + 'enable': None + } + + The value should be `None` if there is no further completion at some + point. If all values in the dictionary are None, it is also possible to + use a set instead. + + Values in this data structure can be a completers as well. + """ + options: Dict[str, Optional[Completer]] = {} + for key, value in data.items(): + if isinstance(value, Completer): + options[key] = value + elif isinstance(value, dict): + options[key] = cls.from_nested_dict(value) + elif isinstance(value, set): + options[key] = cls.from_nested_dict({item: None for item in value}) + else: + assert value is None + options[key] = None + + return cls(options) + + def get_completions( + self, document: Document, complete_event: CompleteEvent + ) -> Iterable[Completion]: + # Split document. + text = document.text_before_cursor.lstrip() + stripped_len = len(document.text_before_cursor) - len(text) + + # If there is a space, check for the first term, and use a + # subcompleter. + if " " in text: + first_term = text.split()[0] + completer = self.options.get(first_term) + + # If we have a sub completer, use this for the completions. + if completer is not None: + remaining_text = text[len(first_term) :].lstrip() + move_cursor = len(text) - len(remaining_text) + stripped_len + + new_document = Document( + remaining_text, + cursor_position=document.cursor_position - move_cursor, + ) + + for c in completer.get_completions(new_document, complete_event): + yield c + + # No space in the input: behave exactly like `WordCompleter`. + else: + completer = WordCompleter( + list(self.options.keys()), ignore_case=self.ignore_case + ) + for c in completer.get_completions(document, complete_event): + yield c diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/word_completer.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/word_completer.py index fd35ed50a6..28c133e921 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/word_completer.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/word_completer.py @@ -1,93 +1,93 @@ -from typing import Callable, Iterable, List, Mapping, Optional, Pattern, Union - -from prompt_toolkit.completion import CompleteEvent, Completer, Completion -from prompt_toolkit.document import Document -from prompt_toolkit.formatted_text import AnyFormattedText - -__all__ = [ - "WordCompleter", -] - - -class WordCompleter(Completer): - """ - Simple autocompletion on a list of words. - - :param words: List of words or callable that returns a list of words. - :param ignore_case: If True, case-insensitive completion. - :param meta_dict: Optional dict mapping words to their meta-text. (This - should map strings to strings or formatted text.) - :param WORD: When True, use WORD characters. - :param sentence: When True, don't complete by comparing the word before the - cursor, but by comparing all the text before the cursor. In this case, - the list of words is just a list of strings, where each string can - contain spaces. (Can not be used together with the WORD option.) - :param match_middle: When True, match not only the start, but also in the - middle of the word. - :param pattern: Optional compiled regex for finding the word before - the cursor to complete. When given, use this regex pattern instead of - default one (see document._FIND_WORD_RE) - """ - - def __init__( - self, - words: Union[List[str], Callable[[], List[str]]], - ignore_case: bool = False, - display_dict: Optional[Mapping[str, AnyFormattedText]] = None, - meta_dict: Optional[Mapping[str, AnyFormattedText]] = None, - WORD: bool = False, - sentence: bool = False, - match_middle: bool = False, - pattern: Optional[Pattern[str]] = None, - ) -> None: - - assert not (WORD and sentence) - - self.words = words - self.ignore_case = ignore_case - self.display_dict = display_dict or {} - self.meta_dict = meta_dict or {} - self.WORD = WORD - self.sentence = sentence - self.match_middle = match_middle - self.pattern = pattern - - def get_completions( - self, document: Document, complete_event: CompleteEvent - ) -> Iterable[Completion]: - # Get list of words. - words = self.words - if callable(words): - words = words() - - # Get word/text before cursor. - if self.sentence: - word_before_cursor = document.text_before_cursor - else: - word_before_cursor = document.get_word_before_cursor( - WORD=self.WORD, pattern=self.pattern - ) - - if self.ignore_case: - word_before_cursor = word_before_cursor.lower() - - def word_matches(word: str) -> bool: - """True when the word before the cursor matches.""" - if self.ignore_case: - word = word.lower() - - if self.match_middle: - return word_before_cursor in word - else: - return word.startswith(word_before_cursor) - - for a in words: - if word_matches(a): - display = self.display_dict.get(a, a) - display_meta = self.meta_dict.get(a, "") - yield Completion( +from typing import Callable, Iterable, List, Mapping, Optional, Pattern, Union + +from prompt_toolkit.completion import CompleteEvent, Completer, Completion +from prompt_toolkit.document import Document +from prompt_toolkit.formatted_text import AnyFormattedText + +__all__ = [ + "WordCompleter", +] + + +class WordCompleter(Completer): + """ + Simple autocompletion on a list of words. + + :param words: List of words or callable that returns a list of words. + :param ignore_case: If True, case-insensitive completion. + :param meta_dict: Optional dict mapping words to their meta-text. (This + should map strings to strings or formatted text.) + :param WORD: When True, use WORD characters. + :param sentence: When True, don't complete by comparing the word before the + cursor, but by comparing all the text before the cursor. In this case, + the list of words is just a list of strings, where each string can + contain spaces. (Can not be used together with the WORD option.) + :param match_middle: When True, match not only the start, but also in the + middle of the word. + :param pattern: Optional compiled regex for finding the word before + the cursor to complete. When given, use this regex pattern instead of + default one (see document._FIND_WORD_RE) + """ + + def __init__( + self, + words: Union[List[str], Callable[[], List[str]]], + ignore_case: bool = False, + display_dict: Optional[Mapping[str, AnyFormattedText]] = None, + meta_dict: Optional[Mapping[str, AnyFormattedText]] = None, + WORD: bool = False, + sentence: bool = False, + match_middle: bool = False, + pattern: Optional[Pattern[str]] = None, + ) -> None: + + assert not (WORD and sentence) + + self.words = words + self.ignore_case = ignore_case + self.display_dict = display_dict or {} + self.meta_dict = meta_dict or {} + self.WORD = WORD + self.sentence = sentence + self.match_middle = match_middle + self.pattern = pattern + + def get_completions( + self, document: Document, complete_event: CompleteEvent + ) -> Iterable[Completion]: + # Get list of words. + words = self.words + if callable(words): + words = words() + + # Get word/text before cursor. + if self.sentence: + word_before_cursor = document.text_before_cursor + else: + word_before_cursor = document.get_word_before_cursor( + WORD=self.WORD, pattern=self.pattern + ) + + if self.ignore_case: + word_before_cursor = word_before_cursor.lower() + + def word_matches(word: str) -> bool: + """True when the word before the cursor matches.""" + if self.ignore_case: + word = word.lower() + + if self.match_middle: + return word_before_cursor in word + else: + return word.startswith(word_before_cursor) + + for a in words: + if word_matches(a): + display = self.display_dict.get(a, a) + display_meta = self.meta_dict.get(a, "") + yield Completion( text=a, start_position=-len(word_before_cursor), - display=display, - display_meta=display_meta, - ) + display=display, + display_meta=display_meta, + ) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/completers/__init__.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/completers/__init__.py index 27faea27cf..50ca2f9127 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/completers/__init__.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/completers/__init__.py @@ -1,3 +1,3 @@ -from .system import SystemCompleter - -__all__ = ["SystemCompleter"] +from .system import SystemCompleter + +__all__ = ["SystemCompleter"] diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/completers/system.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/completers/system.py index fe0d850823..9e63268b22 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/completers/system.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/completers/system.py @@ -1,62 +1,62 @@ -from prompt_toolkit.completion.filesystem import ExecutableCompleter, PathCompleter -from prompt_toolkit.contrib.regular_languages.compiler import compile -from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter - -__all__ = [ - "SystemCompleter", -] - - -class SystemCompleter(GrammarCompleter): - """ - Completer for system commands. - """ - - def __init__(self) -> None: - # Compile grammar. - g = compile( - r""" - # First we have an executable. - (?P<executable>[^\s]+) - - # Ignore literals in between. - ( - \s+ - ("[^"]*" | '[^']*' | [^'"]+ ) - )* - - \s+ - - # Filename as parameters. - ( - (?P<filename>[^\s]+) | - "(?P<double_quoted_filename>[^\s]+)" | - '(?P<single_quoted_filename>[^\s]+)' - ) - """, - escape_funcs={ - "double_quoted_filename": (lambda string: string.replace('"', '\\"')), - "single_quoted_filename": (lambda string: string.replace("'", "\\'")), - }, - unescape_funcs={ - "double_quoted_filename": ( - lambda string: string.replace('\\"', '"') - ), # XXX: not entirely correct. - "single_quoted_filename": (lambda string: string.replace("\\'", "'")), - }, - ) - - # Create GrammarCompleter - super().__init__( - g, - { - "executable": ExecutableCompleter(), - "filename": PathCompleter(only_directories=False, expanduser=True), - "double_quoted_filename": PathCompleter( - only_directories=False, expanduser=True - ), - "single_quoted_filename": PathCompleter( - only_directories=False, expanduser=True - ), - }, - ) +from prompt_toolkit.completion.filesystem import ExecutableCompleter, PathCompleter +from prompt_toolkit.contrib.regular_languages.compiler import compile +from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter + +__all__ = [ + "SystemCompleter", +] + + +class SystemCompleter(GrammarCompleter): + """ + Completer for system commands. + """ + + def __init__(self) -> None: + # Compile grammar. + g = compile( + r""" + # First we have an executable. + (?P<executable>[^\s]+) + + # Ignore literals in between. + ( + \s+ + ("[^"]*" | '[^']*' | [^'"]+ ) + )* + + \s+ + + # Filename as parameters. + ( + (?P<filename>[^\s]+) | + "(?P<double_quoted_filename>[^\s]+)" | + '(?P<single_quoted_filename>[^\s]+)' + ) + """, + escape_funcs={ + "double_quoted_filename": (lambda string: string.replace('"', '\\"')), + "single_quoted_filename": (lambda string: string.replace("'", "\\'")), + }, + unescape_funcs={ + "double_quoted_filename": ( + lambda string: string.replace('\\"', '"') + ), # XXX: not entirely correct. + "single_quoted_filename": (lambda string: string.replace("\\'", "'")), + }, + ) + + # Create GrammarCompleter + super().__init__( + g, + { + "executable": ExecutableCompleter(), + "filename": PathCompleter(only_directories=False, expanduser=True), + "double_quoted_filename": PathCompleter( + only_directories=False, expanduser=True + ), + "single_quoted_filename": PathCompleter( + only_directories=False, expanduser=True + ), + }, + ) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/regular_languages/__init__.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/regular_languages/__init__.py index df2171ac0c..3f306142f7 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/regular_languages/__init__.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/regular_languages/__init__.py @@ -1,77 +1,77 @@ -r""" -Tool for expressing the grammar of an input as a regular language. -================================================================== - -The grammar for the input of many simple command line interfaces can be -expressed by a regular language. Examples are PDB (the Python debugger); a -simple (bash-like) shell with "pwd", "cd", "cat" and "ls" commands; arguments -that you can pass to an executable; etc. It is possible to use regular -expressions for validation and parsing of such a grammar. (More about regular -languages: http://en.wikipedia.org/wiki/Regular_language) - -Example -------- - -Let's take the pwd/cd/cat/ls example. We want to have a shell that accepts -these three commands. "cd" is followed by a quoted directory name and "cat" is -followed by a quoted file name. (We allow quotes inside the filename when -they're escaped with a backslash.) We could define the grammar using the -following regular expression:: - - grammar = \s* ( - pwd | - ls | - (cd \s+ " ([^"]|\.)+ ") | - (cat \s+ " ([^"]|\.)+ ") - ) \s* - - -What can we do with this grammar? ---------------------------------- - -- Syntax highlighting: We could use this for instance to give file names - different colour. -- Parse the result: .. We can extract the file names and commands by using a - regular expression with named groups. -- Input validation: .. Don't accept anything that does not match this grammar. - When combined with a parser, we can also recursively do - filename validation (and accept only existing files.) -- Autocompletion: .... Each part of the grammar can have its own autocompleter. - "cat" has to be completed using file names, while "cd" - has to be completed using directory names. - -How does it work? ------------------ - -As a user of this library, you have to define the grammar of the input as a -regular expression. The parts of this grammar where autocompletion, validation -or any other processing is required need to be marked using a regex named -group. Like ``(?P<varname>...)`` for instance. - -When the input is processed for validation (for instance), the regex will -execute, the named group is captured, and the validator associated with this -named group will test the captured string. - -There is one tricky bit: - - Often we operate on incomplete input (this is by definition the case for - autocompletion) and we have to decide for the cursor position in which - possible state the grammar it could be and in which way variables could be - matched up to that point. - -To solve this problem, the compiler takes the original regular expression and -translates it into a set of other regular expressions which each match certain -prefixes of the original regular expression. We generate one prefix regular -expression for every named variable (with this variable being the end of that -expression). - - -TODO: some examples of: - - How to create a highlighter from this grammar. - - How to create a validator from this grammar. - - How to create an autocompleter from this grammar. - - How to create a parser from this grammar. -""" -from .compiler import compile - -__all__ = ["compile"] +r""" +Tool for expressing the grammar of an input as a regular language. +================================================================== + +The grammar for the input of many simple command line interfaces can be +expressed by a regular language. Examples are PDB (the Python debugger); a +simple (bash-like) shell with "pwd", "cd", "cat" and "ls" commands; arguments +that you can pass to an executable; etc. It is possible to use regular +expressions for validation and parsing of such a grammar. (More about regular +languages: http://en.wikipedia.org/wiki/Regular_language) + +Example +------- + +Let's take the pwd/cd/cat/ls example. We want to have a shell that accepts +these three commands. "cd" is followed by a quoted directory name and "cat" is +followed by a quoted file name. (We allow quotes inside the filename when +they're escaped with a backslash.) We could define the grammar using the +following regular expression:: + + grammar = \s* ( + pwd | + ls | + (cd \s+ " ([^"]|\.)+ ") | + (cat \s+ " ([^"]|\.)+ ") + ) \s* + + +What can we do with this grammar? +--------------------------------- + +- Syntax highlighting: We could use this for instance to give file names + different colour. +- Parse the result: .. We can extract the file names and commands by using a + regular expression with named groups. +- Input validation: .. Don't accept anything that does not match this grammar. + When combined with a parser, we can also recursively do + filename validation (and accept only existing files.) +- Autocompletion: .... Each part of the grammar can have its own autocompleter. + "cat" has to be completed using file names, while "cd" + has to be completed using directory names. + +How does it work? +----------------- + +As a user of this library, you have to define the grammar of the input as a +regular expression. The parts of this grammar where autocompletion, validation +or any other processing is required need to be marked using a regex named +group. Like ``(?P<varname>...)`` for instance. + +When the input is processed for validation (for instance), the regex will +execute, the named group is captured, and the validator associated with this +named group will test the captured string. + +There is one tricky bit: + + Often we operate on incomplete input (this is by definition the case for + autocompletion) and we have to decide for the cursor position in which + possible state the grammar it could be and in which way variables could be + matched up to that point. + +To solve this problem, the compiler takes the original regular expression and +translates it into a set of other regular expressions which each match certain +prefixes of the original regular expression. We generate one prefix regular +expression for every named variable (with this variable being the end of that +expression). + + +TODO: some examples of: + - How to create a highlighter from this grammar. + - How to create a validator from this grammar. + - How to create an autocompleter from this grammar. + - How to create a parser from this grammar. +""" +from .compiler import compile + +__all__ = ["compile"] diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/regular_languages/compiler.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/regular_languages/compiler.py index 2bd567b618..a6eb77127a 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/regular_languages/compiler.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/regular_languages/compiler.py @@ -1,573 +1,573 @@ -r""" -Compiler for a regular grammar. - -Example usage:: - - # Create and compile grammar. - p = compile('add \s+ (?P<var1>[^\s]+) \s+ (?P<var2>[^\s]+)') - - # Match input string. - m = p.match('add 23 432') - - # Get variables. - m.variables().get('var1') # Returns "23" - m.variables().get('var2') # Returns "432" - - -Partial matches are possible:: - - # Create and compile grammar. - p = compile(''' - # Operators with two arguments. - ((?P<operator1>[^\s]+) \s+ (?P<var1>[^\s]+) \s+ (?P<var2>[^\s]+)) | - - # Operators with only one arguments. - ((?P<operator2>[^\s]+) \s+ (?P<var1>[^\s]+)) - ''') - - # Match partial input string. - m = p.match_prefix('add 23') - - # Get variables. (Notice that both operator1 and operator2 contain the - # value "add".) This is because our input is incomplete, and we don't know - # yet in which rule of the regex we we'll end up. It could also be that - # `operator1` and `operator2` have a different autocompleter and we want to - # call all possible autocompleters that would result in valid input.) - m.variables().get('var1') # Returns "23" - m.variables().get('operator1') # Returns "add" - m.variables().get('operator2') # Returns "add" - -""" -import re -from typing import Callable, Dict, Iterable, Iterator, List -from typing import Match as RegexMatch +r""" +Compiler for a regular grammar. + +Example usage:: + + # Create and compile grammar. + p = compile('add \s+ (?P<var1>[^\s]+) \s+ (?P<var2>[^\s]+)') + + # Match input string. + m = p.match('add 23 432') + + # Get variables. + m.variables().get('var1') # Returns "23" + m.variables().get('var2') # Returns "432" + + +Partial matches are possible:: + + # Create and compile grammar. + p = compile(''' + # Operators with two arguments. + ((?P<operator1>[^\s]+) \s+ (?P<var1>[^\s]+) \s+ (?P<var2>[^\s]+)) | + + # Operators with only one arguments. + ((?P<operator2>[^\s]+) \s+ (?P<var1>[^\s]+)) + ''') + + # Match partial input string. + m = p.match_prefix('add 23') + + # Get variables. (Notice that both operator1 and operator2 contain the + # value "add".) This is because our input is incomplete, and we don't know + # yet in which rule of the regex we we'll end up. It could also be that + # `operator1` and `operator2` have a different autocompleter and we want to + # call all possible autocompleters that would result in valid input.) + m.variables().get('var1') # Returns "23" + m.variables().get('operator1') # Returns "add" + m.variables().get('operator2') # Returns "add" + +""" +import re +from typing import Callable, Dict, Iterable, Iterator, List +from typing import Match as RegexMatch from typing import Optional, Pattern, Tuple - -from .regex_parser import ( - AnyNode, - Lookahead, - Node, - NodeSequence, - Regex, - Repeat, - Variable, - parse_regex, - tokenize_regex, -) - -__all__ = [ - "compile", -] - - -# Name of the named group in the regex, matching trailing input. -# (Trailing input is when the input contains characters after the end of the -# expression has been matched.) -_INVALID_TRAILING_INPUT = "invalid_trailing" - -EscapeFuncDict = Dict[str, Callable[[str], str]] - - -class _CompiledGrammar: - """ - Compiles a grammar. This will take the parse tree of a regular expression - and compile the grammar. - - :param root_node: :class~`.regex_parser.Node` instance. - :param escape_funcs: `dict` mapping variable names to escape callables. - :param unescape_funcs: `dict` mapping variable names to unescape callables. - """ - - def __init__( - self, - root_node: Node, - escape_funcs: Optional[EscapeFuncDict] = None, - unescape_funcs: Optional[EscapeFuncDict] = None, - ) -> None: - - self.root_node = root_node - self.escape_funcs = escape_funcs or {} - self.unescape_funcs = unescape_funcs or {} - - #: Dictionary that will map the regex names to Node instances. - self._group_names_to_nodes: Dict[ - str, str - ] = {} # Maps regex group names to varnames. - counter = [0] - - def create_group_func(node: Variable) -> str: - name = "n%s" % counter[0] - self._group_names_to_nodes[name] = node.varname - counter[0] += 1 - return name - - # Compile regex strings. - self._re_pattern = "^%s$" % self._transform(root_node, create_group_func) - self._re_prefix_patterns = list( - self._transform_prefix(root_node, create_group_func) - ) - - # Compile the regex itself. - flags = re.DOTALL # Note that we don't need re.MULTILINE! (^ and $ - # still represent the start and end of input text.) - self._re = re.compile(self._re_pattern, flags) - self._re_prefix = [re.compile(t, flags) for t in self._re_prefix_patterns] - - # We compile one more set of regexes, similar to `_re_prefix`, but accept any trailing - # input. This will ensure that we can still highlight the input correctly, even when the - # input contains some additional characters at the end that don't match the grammar.) - self._re_prefix_with_trailing_input = [ - re.compile( - r"(?:%s)(?P<%s>.*?)$" % (t.rstrip("$"), _INVALID_TRAILING_INPUT), flags - ) - for t in self._re_prefix_patterns - ] - - def escape(self, varname: str, value: str) -> str: - """ - Escape `value` to fit in the place of this variable into the grammar. - """ - f = self.escape_funcs.get(varname) - return f(value) if f else value - - def unescape(self, varname: str, value: str) -> str: - """ - Unescape `value`. - """ - f = self.unescape_funcs.get(varname) - return f(value) if f else value - - @classmethod - def _transform( - cls, root_node: Node, create_group_func: Callable[[Variable], str] - ) -> str: - """ - Turn a :class:`Node` object into a regular expression. - - :param root_node: The :class:`Node` instance for which we generate the grammar. - :param create_group_func: A callable which takes a `Node` and returns the next - free name for this node. - """ - - def transform(node: Node) -> str: - # Turn `AnyNode` into an OR. - if isinstance(node, AnyNode): - return "(?:%s)" % "|".join(transform(c) for c in node.children) - - # Concatenate a `NodeSequence` - elif isinstance(node, NodeSequence): - return "".join(transform(c) for c in node.children) - - # For Regex and Lookahead nodes, just insert them literally. - elif isinstance(node, Regex): - return node.regex - - elif isinstance(node, Lookahead): - before = "(?!" if node.negative else "(=" - return before + transform(node.childnode) + ")" - - # A `Variable` wraps the children into a named group. - elif isinstance(node, Variable): - return "(?P<%s>%s)" % ( - create_group_func(node), - transform(node.childnode), - ) - - # `Repeat`. - elif isinstance(node, Repeat): - if node.max_repeat is None: - if node.min_repeat == 0: - repeat_sign = "*" - elif node.min_repeat == 1: - repeat_sign = "+" - else: - repeat_sign = "{%i,%s}" % ( - node.min_repeat, - ("" if node.max_repeat is None else str(node.max_repeat)), - ) - - return "(?:%s)%s%s" % ( - transform(node.childnode), - repeat_sign, - ("" if node.greedy else "?"), - ) - else: - raise TypeError("Got %r" % (node,)) - - return transform(root_node) - - @classmethod - def _transform_prefix( - cls, root_node: Node, create_group_func: Callable[[Variable], str] - ) -> Iterable[str]: - """ - Yield all the regular expressions matching a prefix of the grammar - defined by the `Node` instance. - - For each `Variable`, one regex pattern will be generated, with this - named group at the end. This is required because a regex engine will - terminate once a match is found. For autocompletion however, we need - the matches for all possible paths, so that we can provide completions - for each `Variable`. - - - So, in the case of an `Any` (`A|B|C)', we generate a pattern for each - clause. This is one for `A`, one for `B` and one for `C`. Unless some - groups don't contain a `Variable`, then these can be merged together. - - In the case of a `NodeSequence` (`ABC`), we generate a pattern for - each prefix that ends with a variable, and one pattern for the whole - sequence. So, that's one for `A`, one for `AB` and one for `ABC`. - - :param root_node: The :class:`Node` instance for which we generate the grammar. - :param create_group_func: A callable which takes a `Node` and returns the next - free name for this node. - """ - - def contains_variable(node: Node) -> bool: - if isinstance(node, Regex): - return False - elif isinstance(node, Variable): - return True - elif isinstance(node, (Lookahead, Repeat)): - return contains_variable(node.childnode) - elif isinstance(node, (NodeSequence, AnyNode)): - return any(contains_variable(child) for child in node.children) - - return False - - def transform(node: Node) -> Iterable[str]: - # Generate separate pattern for all terms that contain variables - # within this OR. Terms that don't contain a variable can be merged - # together in one pattern. - if isinstance(node, AnyNode): - # If we have a definition like: - # (?P<name> .*) | (?P<city> .*) - # Then we want to be able to generate completions for both the - # name as well as the city. We do this by yielding two - # different regular expressions, because the engine won't - # follow multiple paths, if multiple are possible. - children_with_variable = [] - children_without_variable = [] - for c in node.children: - if contains_variable(c): - children_with_variable.append(c) - else: - children_without_variable.append(c) - - for c in children_with_variable: - yield from transform(c) - - # Merge options without variable together. - if children_without_variable: - yield "|".join( - r for c in children_without_variable for r in transform(c) - ) - - # For a sequence, generate a pattern for each prefix that ends with - # a variable + one pattern of the complete sequence. - # (This is because, for autocompletion, we match the text before - # the cursor, and completions are given for the variable that we - # match right before the cursor.) - elif isinstance(node, NodeSequence): - # For all components in the sequence, compute prefix patterns, - # as well as full patterns. - complete = [cls._transform(c, create_group_func) for c in node.children] - prefixes = [list(transform(c)) for c in node.children] - variable_nodes = [contains_variable(c) for c in node.children] - - # If any child is contains a variable, we should yield a - # pattern up to that point, so that we are sure this will be - # matched. - for i in range(len(node.children)): - if variable_nodes[i]: - for c_str in prefixes[i]: - yield "".join(complete[:i]) + c_str - - # If there are non-variable nodes, merge all the prefixes into - # one pattern. If the input is: "[part1] [part2] [part3]", then - # this gets compiled into: - # (complete1 + (complete2 + (complete3 | partial3) | partial2) | partial1 ) - # For nodes that contain a variable, we skip the "|partial" - # part here, because thees are matched with the previous - # patterns. - if not all(variable_nodes): - result = [] - - # Start with complete patterns. - for i in range(len(node.children)): - result.append("(?:") - result.append(complete[i]) - - # Add prefix patterns. - for i in range(len(node.children) - 1, -1, -1): - if variable_nodes[i]: - # No need to yield a prefix for this one, we did - # the variable prefixes earlier. - result.append(")") - else: - result.append("|(?:") - # If this yields multiple, we should yield all combinations. - assert len(prefixes[i]) == 1 - result.append(prefixes[i][0]) - result.append("))") - - yield "".join(result) - - elif isinstance(node, Regex): - yield "(?:%s)?" % node.regex - - elif isinstance(node, Lookahead): - if node.negative: - yield "(?!%s)" % cls._transform(node.childnode, create_group_func) - else: - # Not sure what the correct semantics are in this case. - # (Probably it's not worth implementing this.) - raise Exception("Positive lookahead not yet supported.") - - elif isinstance(node, Variable): - # (Note that we should not append a '?' here. the 'transform' - # method will already recursively do that.) - for c_str in transform(node.childnode): - yield "(?P<%s>%s)" % (create_group_func(node), c_str) - - elif isinstance(node, Repeat): - # If we have a repetition of 8 times. That would mean that the - # current input could have for instance 7 times a complete - # match, followed by a partial match. - prefix = cls._transform(node.childnode, create_group_func) - - if node.max_repeat == 1: - yield from transform(node.childnode) - else: - for c_str in transform(node.childnode): - if node.max_repeat: - repeat_sign = "{,%i}" % (node.max_repeat - 1) - else: - repeat_sign = "*" - yield "(?:%s)%s%s%s" % ( - prefix, - repeat_sign, - ("" if node.greedy else "?"), - c_str, - ) - - else: - raise TypeError("Got %r" % node) - - for r in transform(root_node): - yield "^(?:%s)$" % r - - def match(self, string: str) -> Optional["Match"]: - """ - Match the string with the grammar. - Returns a :class:`Match` instance or `None` when the input doesn't match the grammar. - - :param string: The input string. - """ - m = self._re.match(string) - - if m: - return Match( - string, [(self._re, m)], self._group_names_to_nodes, self.unescape_funcs - ) - return None - - def match_prefix(self, string: str) -> Optional["Match"]: - """ - Do a partial match of the string with the grammar. The returned - :class:`Match` instance can contain multiple representations of the - match. This will never return `None`. If it doesn't match at all, the "trailing input" - part will capture all of the input. - - :param string: The input string. - """ - # First try to match using `_re_prefix`. If nothing is found, use the patterns that - # also accept trailing characters. - for patterns in [self._re_prefix, self._re_prefix_with_trailing_input]: - matches = [(r, r.match(string)) for r in patterns] - matches2 = [(r, m) for r, m in matches if m] - - if matches2 != []: - return Match( - string, matches2, self._group_names_to_nodes, self.unescape_funcs - ) - - return None - - -class Match: - """ - :param string: The input string. - :param re_matches: List of (compiled_re_pattern, re_match) tuples. - :param group_names_to_nodes: Dictionary mapping all the re group names to the matching Node instances. - """ - - def __init__( - self, - string: str, - re_matches: List[Tuple[Pattern[str], RegexMatch[str]]], - group_names_to_nodes: Dict[str, str], - unescape_funcs: Dict[str, Callable[[str], str]], - ): - self.string = string - self._re_matches = re_matches - self._group_names_to_nodes = group_names_to_nodes - self._unescape_funcs = unescape_funcs - - def _nodes_to_regs(self) -> List[Tuple[str, Tuple[int, int]]]: - """ - Return a list of (varname, reg) tuples. - """ - - def get_tuples() -> Iterable[Tuple[str, Tuple[int, int]]]: - for r, re_match in self._re_matches: - for group_name, group_index in r.groupindex.items(): - if group_name != _INVALID_TRAILING_INPUT: - regs = re_match.regs - reg = regs[group_index] - node = self._group_names_to_nodes[group_name] - yield (node, reg) - - return list(get_tuples()) - - def _nodes_to_values(self) -> List[Tuple[str, str, Tuple[int, int]]]: - """ - Returns list of (Node, string_value) tuples. - """ - - def is_none(sl: Tuple[int, int]) -> bool: - return sl[0] == -1 and sl[1] == -1 - - def get(sl: Tuple[int, int]) -> str: - return self.string[sl[0] : sl[1]] - - return [ - (varname, get(slice), slice) - for varname, slice in self._nodes_to_regs() - if not is_none(slice) - ] - - def _unescape(self, varname: str, value: str) -> str: - unwrapper = self._unescape_funcs.get(varname) - return unwrapper(value) if unwrapper else value - - def variables(self) -> "Variables": - """ - Returns :class:`Variables` instance. - """ - return Variables( - [(k, self._unescape(k, v), sl) for k, v, sl in self._nodes_to_values()] - ) - - def trailing_input(self) -> Optional["MatchVariable"]: - """ - Get the `MatchVariable` instance, representing trailing input, if there is any. - "Trailing input" is input at the end that does not match the grammar anymore, but - when this is removed from the end of the input, the input would be a valid string. - """ - slices: List[Tuple[int, int]] = [] - - # Find all regex group for the name _INVALID_TRAILING_INPUT. - for r, re_match in self._re_matches: - for group_name, group_index in r.groupindex.items(): - if group_name == _INVALID_TRAILING_INPUT: - slices.append(re_match.regs[group_index]) - - # Take the smallest part. (Smaller trailing text means that a larger input has - # been matched, so that is better.) - if slices: - slice = (max(i[0] for i in slices), max(i[1] for i in slices)) - value = self.string[slice[0] : slice[1]] - return MatchVariable("<trailing_input>", value, slice) - return None - - def end_nodes(self) -> Iterable["MatchVariable"]: - """ - Yields `MatchVariable` instances for all the nodes having their end - position at the end of the input string. - """ - for varname, reg in self._nodes_to_regs(): - # If this part goes until the end of the input string. - if reg[1] == len(self.string): - value = self._unescape(varname, self.string[reg[0] : reg[1]]) - yield MatchVariable(varname, value, (reg[0], reg[1])) - - -class Variables: - def __init__(self, tuples: List[Tuple[str, str, Tuple[int, int]]]) -> None: - #: List of (varname, value, slice) tuples. - self._tuples = tuples - - def __repr__(self) -> str: - return "%s(%s)" % ( - self.__class__.__name__, - ", ".join("%s=%r" % (k, v) for k, v, _ in self._tuples), - ) - - def get(self, key: str, default: Optional[str] = None) -> Optional[str]: - items = self.getall(key) - return items[0] if items else default - - def getall(self, key: str) -> List[str]: - return [v for k, v, _ in self._tuples if k == key] - - def __getitem__(self, key: str) -> Optional[str]: - return self.get(key) - - def __iter__(self) -> Iterator["MatchVariable"]: - """ - Yield `MatchVariable` instances. - """ - for varname, value, slice in self._tuples: - yield MatchVariable(varname, value, slice) - - -class MatchVariable: - """ - Represents a match of a variable in the grammar. - - :param varname: (string) Name of the variable. - :param value: (string) Value of this variable. - :param slice: (start, stop) tuple, indicating the position of this variable - in the input string. - """ - - def __init__(self, varname: str, value: str, slice: Tuple[int, int]) -> None: - self.varname = varname - self.value = value - self.slice = slice - - self.start = self.slice[0] - self.stop = self.slice[1] - - def __repr__(self) -> str: - return "%s(%r, %r)" % (self.__class__.__name__, self.varname, self.value) - - -def compile( - expression: str, - escape_funcs: Optional[EscapeFuncDict] = None, - unescape_funcs: Optional[EscapeFuncDict] = None, -) -> _CompiledGrammar: - """ - Compile grammar (given as regex string), returning a `CompiledGrammar` - instance. - """ - return _compile_from_parse_tree( - parse_regex(tokenize_regex(expression)), - escape_funcs=escape_funcs, - unescape_funcs=unescape_funcs, - ) - - -def _compile_from_parse_tree( - root_node: Node, - escape_funcs: Optional[EscapeFuncDict] = None, - unescape_funcs: Optional[EscapeFuncDict] = None, -) -> _CompiledGrammar: - """ - Compile grammar (given as parse tree), returning a `CompiledGrammar` - instance. - """ - return _CompiledGrammar( - root_node, escape_funcs=escape_funcs, unescape_funcs=unescape_funcs - ) + +from .regex_parser import ( + AnyNode, + Lookahead, + Node, + NodeSequence, + Regex, + Repeat, + Variable, + parse_regex, + tokenize_regex, +) + +__all__ = [ + "compile", +] + + +# Name of the named group in the regex, matching trailing input. +# (Trailing input is when the input contains characters after the end of the +# expression has been matched.) +_INVALID_TRAILING_INPUT = "invalid_trailing" + +EscapeFuncDict = Dict[str, Callable[[str], str]] + + +class _CompiledGrammar: + """ + Compiles a grammar. This will take the parse tree of a regular expression + and compile the grammar. + + :param root_node: :class~`.regex_parser.Node` instance. + :param escape_funcs: `dict` mapping variable names to escape callables. + :param unescape_funcs: `dict` mapping variable names to unescape callables. + """ + + def __init__( + self, + root_node: Node, + escape_funcs: Optional[EscapeFuncDict] = None, + unescape_funcs: Optional[EscapeFuncDict] = None, + ) -> None: + + self.root_node = root_node + self.escape_funcs = escape_funcs or {} + self.unescape_funcs = unescape_funcs or {} + + #: Dictionary that will map the regex names to Node instances. + self._group_names_to_nodes: Dict[ + str, str + ] = {} # Maps regex group names to varnames. + counter = [0] + + def create_group_func(node: Variable) -> str: + name = "n%s" % counter[0] + self._group_names_to_nodes[name] = node.varname + counter[0] += 1 + return name + + # Compile regex strings. + self._re_pattern = "^%s$" % self._transform(root_node, create_group_func) + self._re_prefix_patterns = list( + self._transform_prefix(root_node, create_group_func) + ) + + # Compile the regex itself. + flags = re.DOTALL # Note that we don't need re.MULTILINE! (^ and $ + # still represent the start and end of input text.) + self._re = re.compile(self._re_pattern, flags) + self._re_prefix = [re.compile(t, flags) for t in self._re_prefix_patterns] + + # We compile one more set of regexes, similar to `_re_prefix`, but accept any trailing + # input. This will ensure that we can still highlight the input correctly, even when the + # input contains some additional characters at the end that don't match the grammar.) + self._re_prefix_with_trailing_input = [ + re.compile( + r"(?:%s)(?P<%s>.*?)$" % (t.rstrip("$"), _INVALID_TRAILING_INPUT), flags + ) + for t in self._re_prefix_patterns + ] + + def escape(self, varname: str, value: str) -> str: + """ + Escape `value` to fit in the place of this variable into the grammar. + """ + f = self.escape_funcs.get(varname) + return f(value) if f else value + + def unescape(self, varname: str, value: str) -> str: + """ + Unescape `value`. + """ + f = self.unescape_funcs.get(varname) + return f(value) if f else value + + @classmethod + def _transform( + cls, root_node: Node, create_group_func: Callable[[Variable], str] + ) -> str: + """ + Turn a :class:`Node` object into a regular expression. + + :param root_node: The :class:`Node` instance for which we generate the grammar. + :param create_group_func: A callable which takes a `Node` and returns the next + free name for this node. + """ + + def transform(node: Node) -> str: + # Turn `AnyNode` into an OR. + if isinstance(node, AnyNode): + return "(?:%s)" % "|".join(transform(c) for c in node.children) + + # Concatenate a `NodeSequence` + elif isinstance(node, NodeSequence): + return "".join(transform(c) for c in node.children) + + # For Regex and Lookahead nodes, just insert them literally. + elif isinstance(node, Regex): + return node.regex + + elif isinstance(node, Lookahead): + before = "(?!" if node.negative else "(=" + return before + transform(node.childnode) + ")" + + # A `Variable` wraps the children into a named group. + elif isinstance(node, Variable): + return "(?P<%s>%s)" % ( + create_group_func(node), + transform(node.childnode), + ) + + # `Repeat`. + elif isinstance(node, Repeat): + if node.max_repeat is None: + if node.min_repeat == 0: + repeat_sign = "*" + elif node.min_repeat == 1: + repeat_sign = "+" + else: + repeat_sign = "{%i,%s}" % ( + node.min_repeat, + ("" if node.max_repeat is None else str(node.max_repeat)), + ) + + return "(?:%s)%s%s" % ( + transform(node.childnode), + repeat_sign, + ("" if node.greedy else "?"), + ) + else: + raise TypeError("Got %r" % (node,)) + + return transform(root_node) + + @classmethod + def _transform_prefix( + cls, root_node: Node, create_group_func: Callable[[Variable], str] + ) -> Iterable[str]: + """ + Yield all the regular expressions matching a prefix of the grammar + defined by the `Node` instance. + + For each `Variable`, one regex pattern will be generated, with this + named group at the end. This is required because a regex engine will + terminate once a match is found. For autocompletion however, we need + the matches for all possible paths, so that we can provide completions + for each `Variable`. + + - So, in the case of an `Any` (`A|B|C)', we generate a pattern for each + clause. This is one for `A`, one for `B` and one for `C`. Unless some + groups don't contain a `Variable`, then these can be merged together. + - In the case of a `NodeSequence` (`ABC`), we generate a pattern for + each prefix that ends with a variable, and one pattern for the whole + sequence. So, that's one for `A`, one for `AB` and one for `ABC`. + + :param root_node: The :class:`Node` instance for which we generate the grammar. + :param create_group_func: A callable which takes a `Node` and returns the next + free name for this node. + """ + + def contains_variable(node: Node) -> bool: + if isinstance(node, Regex): + return False + elif isinstance(node, Variable): + return True + elif isinstance(node, (Lookahead, Repeat)): + return contains_variable(node.childnode) + elif isinstance(node, (NodeSequence, AnyNode)): + return any(contains_variable(child) for child in node.children) + + return False + + def transform(node: Node) -> Iterable[str]: + # Generate separate pattern for all terms that contain variables + # within this OR. Terms that don't contain a variable can be merged + # together in one pattern. + if isinstance(node, AnyNode): + # If we have a definition like: + # (?P<name> .*) | (?P<city> .*) + # Then we want to be able to generate completions for both the + # name as well as the city. We do this by yielding two + # different regular expressions, because the engine won't + # follow multiple paths, if multiple are possible. + children_with_variable = [] + children_without_variable = [] + for c in node.children: + if contains_variable(c): + children_with_variable.append(c) + else: + children_without_variable.append(c) + + for c in children_with_variable: + yield from transform(c) + + # Merge options without variable together. + if children_without_variable: + yield "|".join( + r for c in children_without_variable for r in transform(c) + ) + + # For a sequence, generate a pattern for each prefix that ends with + # a variable + one pattern of the complete sequence. + # (This is because, for autocompletion, we match the text before + # the cursor, and completions are given for the variable that we + # match right before the cursor.) + elif isinstance(node, NodeSequence): + # For all components in the sequence, compute prefix patterns, + # as well as full patterns. + complete = [cls._transform(c, create_group_func) for c in node.children] + prefixes = [list(transform(c)) for c in node.children] + variable_nodes = [contains_variable(c) for c in node.children] + + # If any child is contains a variable, we should yield a + # pattern up to that point, so that we are sure this will be + # matched. + for i in range(len(node.children)): + if variable_nodes[i]: + for c_str in prefixes[i]: + yield "".join(complete[:i]) + c_str + + # If there are non-variable nodes, merge all the prefixes into + # one pattern. If the input is: "[part1] [part2] [part3]", then + # this gets compiled into: + # (complete1 + (complete2 + (complete3 | partial3) | partial2) | partial1 ) + # For nodes that contain a variable, we skip the "|partial" + # part here, because thees are matched with the previous + # patterns. + if not all(variable_nodes): + result = [] + + # Start with complete patterns. + for i in range(len(node.children)): + result.append("(?:") + result.append(complete[i]) + + # Add prefix patterns. + for i in range(len(node.children) - 1, -1, -1): + if variable_nodes[i]: + # No need to yield a prefix for this one, we did + # the variable prefixes earlier. + result.append(")") + else: + result.append("|(?:") + # If this yields multiple, we should yield all combinations. + assert len(prefixes[i]) == 1 + result.append(prefixes[i][0]) + result.append("))") + + yield "".join(result) + + elif isinstance(node, Regex): + yield "(?:%s)?" % node.regex + + elif isinstance(node, Lookahead): + if node.negative: + yield "(?!%s)" % cls._transform(node.childnode, create_group_func) + else: + # Not sure what the correct semantics are in this case. + # (Probably it's not worth implementing this.) + raise Exception("Positive lookahead not yet supported.") + + elif isinstance(node, Variable): + # (Note that we should not append a '?' here. the 'transform' + # method will already recursively do that.) + for c_str in transform(node.childnode): + yield "(?P<%s>%s)" % (create_group_func(node), c_str) + + elif isinstance(node, Repeat): + # If we have a repetition of 8 times. That would mean that the + # current input could have for instance 7 times a complete + # match, followed by a partial match. + prefix = cls._transform(node.childnode, create_group_func) + + if node.max_repeat == 1: + yield from transform(node.childnode) + else: + for c_str in transform(node.childnode): + if node.max_repeat: + repeat_sign = "{,%i}" % (node.max_repeat - 1) + else: + repeat_sign = "*" + yield "(?:%s)%s%s%s" % ( + prefix, + repeat_sign, + ("" if node.greedy else "?"), + c_str, + ) + + else: + raise TypeError("Got %r" % node) + + for r in transform(root_node): + yield "^(?:%s)$" % r + + def match(self, string: str) -> Optional["Match"]: + """ + Match the string with the grammar. + Returns a :class:`Match` instance or `None` when the input doesn't match the grammar. + + :param string: The input string. + """ + m = self._re.match(string) + + if m: + return Match( + string, [(self._re, m)], self._group_names_to_nodes, self.unescape_funcs + ) + return None + + def match_prefix(self, string: str) -> Optional["Match"]: + """ + Do a partial match of the string with the grammar. The returned + :class:`Match` instance can contain multiple representations of the + match. This will never return `None`. If it doesn't match at all, the "trailing input" + part will capture all of the input. + + :param string: The input string. + """ + # First try to match using `_re_prefix`. If nothing is found, use the patterns that + # also accept trailing characters. + for patterns in [self._re_prefix, self._re_prefix_with_trailing_input]: + matches = [(r, r.match(string)) for r in patterns] + matches2 = [(r, m) for r, m in matches if m] + + if matches2 != []: + return Match( + string, matches2, self._group_names_to_nodes, self.unescape_funcs + ) + + return None + + +class Match: + """ + :param string: The input string. + :param re_matches: List of (compiled_re_pattern, re_match) tuples. + :param group_names_to_nodes: Dictionary mapping all the re group names to the matching Node instances. + """ + + def __init__( + self, + string: str, + re_matches: List[Tuple[Pattern[str], RegexMatch[str]]], + group_names_to_nodes: Dict[str, str], + unescape_funcs: Dict[str, Callable[[str], str]], + ): + self.string = string + self._re_matches = re_matches + self._group_names_to_nodes = group_names_to_nodes + self._unescape_funcs = unescape_funcs + + def _nodes_to_regs(self) -> List[Tuple[str, Tuple[int, int]]]: + """ + Return a list of (varname, reg) tuples. + """ + + def get_tuples() -> Iterable[Tuple[str, Tuple[int, int]]]: + for r, re_match in self._re_matches: + for group_name, group_index in r.groupindex.items(): + if group_name != _INVALID_TRAILING_INPUT: + regs = re_match.regs + reg = regs[group_index] + node = self._group_names_to_nodes[group_name] + yield (node, reg) + + return list(get_tuples()) + + def _nodes_to_values(self) -> List[Tuple[str, str, Tuple[int, int]]]: + """ + Returns list of (Node, string_value) tuples. + """ + + def is_none(sl: Tuple[int, int]) -> bool: + return sl[0] == -1 and sl[1] == -1 + + def get(sl: Tuple[int, int]) -> str: + return self.string[sl[0] : sl[1]] + + return [ + (varname, get(slice), slice) + for varname, slice in self._nodes_to_regs() + if not is_none(slice) + ] + + def _unescape(self, varname: str, value: str) -> str: + unwrapper = self._unescape_funcs.get(varname) + return unwrapper(value) if unwrapper else value + + def variables(self) -> "Variables": + """ + Returns :class:`Variables` instance. + """ + return Variables( + [(k, self._unescape(k, v), sl) for k, v, sl in self._nodes_to_values()] + ) + + def trailing_input(self) -> Optional["MatchVariable"]: + """ + Get the `MatchVariable` instance, representing trailing input, if there is any. + "Trailing input" is input at the end that does not match the grammar anymore, but + when this is removed from the end of the input, the input would be a valid string. + """ + slices: List[Tuple[int, int]] = [] + + # Find all regex group for the name _INVALID_TRAILING_INPUT. + for r, re_match in self._re_matches: + for group_name, group_index in r.groupindex.items(): + if group_name == _INVALID_TRAILING_INPUT: + slices.append(re_match.regs[group_index]) + + # Take the smallest part. (Smaller trailing text means that a larger input has + # been matched, so that is better.) + if slices: + slice = (max(i[0] for i in slices), max(i[1] for i in slices)) + value = self.string[slice[0] : slice[1]] + return MatchVariable("<trailing_input>", value, slice) + return None + + def end_nodes(self) -> Iterable["MatchVariable"]: + """ + Yields `MatchVariable` instances for all the nodes having their end + position at the end of the input string. + """ + for varname, reg in self._nodes_to_regs(): + # If this part goes until the end of the input string. + if reg[1] == len(self.string): + value = self._unescape(varname, self.string[reg[0] : reg[1]]) + yield MatchVariable(varname, value, (reg[0], reg[1])) + + +class Variables: + def __init__(self, tuples: List[Tuple[str, str, Tuple[int, int]]]) -> None: + #: List of (varname, value, slice) tuples. + self._tuples = tuples + + def __repr__(self) -> str: + return "%s(%s)" % ( + self.__class__.__name__, + ", ".join("%s=%r" % (k, v) for k, v, _ in self._tuples), + ) + + def get(self, key: str, default: Optional[str] = None) -> Optional[str]: + items = self.getall(key) + return items[0] if items else default + + def getall(self, key: str) -> List[str]: + return [v for k, v, _ in self._tuples if k == key] + + def __getitem__(self, key: str) -> Optional[str]: + return self.get(key) + + def __iter__(self) -> Iterator["MatchVariable"]: + """ + Yield `MatchVariable` instances. + """ + for varname, value, slice in self._tuples: + yield MatchVariable(varname, value, slice) + + +class MatchVariable: + """ + Represents a match of a variable in the grammar. + + :param varname: (string) Name of the variable. + :param value: (string) Value of this variable. + :param slice: (start, stop) tuple, indicating the position of this variable + in the input string. + """ + + def __init__(self, varname: str, value: str, slice: Tuple[int, int]) -> None: + self.varname = varname + self.value = value + self.slice = slice + + self.start = self.slice[0] + self.stop = self.slice[1] + + def __repr__(self) -> str: + return "%s(%r, %r)" % (self.__class__.__name__, self.varname, self.value) + + +def compile( + expression: str, + escape_funcs: Optional[EscapeFuncDict] = None, + unescape_funcs: Optional[EscapeFuncDict] = None, +) -> _CompiledGrammar: + """ + Compile grammar (given as regex string), returning a `CompiledGrammar` + instance. + """ + return _compile_from_parse_tree( + parse_regex(tokenize_regex(expression)), + escape_funcs=escape_funcs, + unescape_funcs=unescape_funcs, + ) + + +def _compile_from_parse_tree( + root_node: Node, + escape_funcs: Optional[EscapeFuncDict] = None, + unescape_funcs: Optional[EscapeFuncDict] = None, +) -> _CompiledGrammar: + """ + Compile grammar (given as parse tree), returning a `CompiledGrammar` + instance. + """ + return _CompiledGrammar( + root_node, escape_funcs=escape_funcs, unescape_funcs=unescape_funcs + ) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/regular_languages/completion.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/regular_languages/completion.py index 9a81ffa759..7aaad71f18 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/regular_languages/completion.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/regular_languages/completion.py @@ -1,94 +1,94 @@ -""" -Completer for a regular grammar. -""" -from typing import Dict, Iterable, List - -from prompt_toolkit.completion import CompleteEvent, Completer, Completion -from prompt_toolkit.document import Document - -from .compiler import Match, _CompiledGrammar - -__all__ = [ - "GrammarCompleter", -] - - -class GrammarCompleter(Completer): - """ - Completer which can be used for autocompletion according to variables in - the grammar. Each variable can have a different autocompleter. - - :param compiled_grammar: `GrammarCompleter` instance. - :param completers: `dict` mapping variable names of the grammar to the - `Completer` instances to be used for each variable. - """ - - def __init__( - self, compiled_grammar: _CompiledGrammar, completers: Dict[str, Completer] - ) -> None: - - self.compiled_grammar = compiled_grammar - self.completers = completers - - def get_completions( - self, document: Document, complete_event: CompleteEvent - ) -> Iterable[Completion]: - m = self.compiled_grammar.match_prefix(document.text_before_cursor) - - if m: - completions = self._remove_duplicates( - self._get_completions_for_match(m, complete_event) - ) - - for c in completions: - yield c - - def _get_completions_for_match( - self, match: Match, complete_event: CompleteEvent - ) -> Iterable[Completion]: - """ - Yield all the possible completions for this input string. - (The completer assumes that the cursor position was at the end of the - input string.) - """ - for match_variable in match.end_nodes(): - varname = match_variable.varname - start = match_variable.start - - completer = self.completers.get(varname) - - if completer: - text = match_variable.value - - # Unwrap text. - unwrapped_text = self.compiled_grammar.unescape(varname, text) - - # Create a document, for the completions API (text/cursor_position) - document = Document(unwrapped_text, len(unwrapped_text)) - - # Call completer - for completion in completer.get_completions(document, complete_event): - new_text = ( - unwrapped_text[: len(text) + completion.start_position] - + completion.text - ) - - # Wrap again. - yield Completion( - text=self.compiled_grammar.escape(varname, new_text), - start_position=start - len(match.string), - display=completion.display, - display_meta=completion.display_meta, - ) - - def _remove_duplicates(self, items: Iterable[Completion]) -> List[Completion]: - """ - Remove duplicates, while keeping the order. - (Sometimes we have duplicates, because the there several matches of the - same grammar, each yielding similar completions.) - """ - result: List[Completion] = [] - for i in items: - if i not in result: - result.append(i) - return result +""" +Completer for a regular grammar. +""" +from typing import Dict, Iterable, List + +from prompt_toolkit.completion import CompleteEvent, Completer, Completion +from prompt_toolkit.document import Document + +from .compiler import Match, _CompiledGrammar + +__all__ = [ + "GrammarCompleter", +] + + +class GrammarCompleter(Completer): + """ + Completer which can be used for autocompletion according to variables in + the grammar. Each variable can have a different autocompleter. + + :param compiled_grammar: `GrammarCompleter` instance. + :param completers: `dict` mapping variable names of the grammar to the + `Completer` instances to be used for each variable. + """ + + def __init__( + self, compiled_grammar: _CompiledGrammar, completers: Dict[str, Completer] + ) -> None: + + self.compiled_grammar = compiled_grammar + self.completers = completers + + def get_completions( + self, document: Document, complete_event: CompleteEvent + ) -> Iterable[Completion]: + m = self.compiled_grammar.match_prefix(document.text_before_cursor) + + if m: + completions = self._remove_duplicates( + self._get_completions_for_match(m, complete_event) + ) + + for c in completions: + yield c + + def _get_completions_for_match( + self, match: Match, complete_event: CompleteEvent + ) -> Iterable[Completion]: + """ + Yield all the possible completions for this input string. + (The completer assumes that the cursor position was at the end of the + input string.) + """ + for match_variable in match.end_nodes(): + varname = match_variable.varname + start = match_variable.start + + completer = self.completers.get(varname) + + if completer: + text = match_variable.value + + # Unwrap text. + unwrapped_text = self.compiled_grammar.unescape(varname, text) + + # Create a document, for the completions API (text/cursor_position) + document = Document(unwrapped_text, len(unwrapped_text)) + + # Call completer + for completion in completer.get_completions(document, complete_event): + new_text = ( + unwrapped_text[: len(text) + completion.start_position] + + completion.text + ) + + # Wrap again. + yield Completion( + text=self.compiled_grammar.escape(varname, new_text), + start_position=start - len(match.string), + display=completion.display, + display_meta=completion.display_meta, + ) + + def _remove_duplicates(self, items: Iterable[Completion]) -> List[Completion]: + """ + Remove duplicates, while keeping the order. + (Sometimes we have duplicates, because the there several matches of the + same grammar, each yielding similar completions.) + """ + result: List[Completion] = [] + for i in items: + if i not in result: + result.append(i) + return result diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/regular_languages/lexer.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/regular_languages/lexer.py index a760d83ed2..1ddeede802 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/regular_languages/lexer.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/regular_languages/lexer.py @@ -1,92 +1,92 @@ -""" -`GrammarLexer` is compatible with other lexers and can be used to highlight -the input using a regular grammar with annotations. -""" -from typing import Callable, Dict, Optional - -from prompt_toolkit.document import Document -from prompt_toolkit.formatted_text.base import StyleAndTextTuples -from prompt_toolkit.formatted_text.utils import split_lines -from prompt_toolkit.lexers import Lexer - -from .compiler import _CompiledGrammar - -__all__ = [ - "GrammarLexer", -] - - -class GrammarLexer(Lexer): - """ - Lexer which can be used for highlighting of fragments according to variables in the grammar. - - (It does not actual lexing of the string, but it exposes an API, compatible - with the Pygments lexer class.) - - :param compiled_grammar: Grammar as returned by the `compile()` function. - :param lexers: Dictionary mapping variable names of the regular grammar to - the lexers that should be used for this part. (This can - call other lexers recursively.) If you wish a part of the - grammar to just get one fragment, use a - `prompt_toolkit.lexers.SimpleLexer`. - """ - - def __init__( - self, - compiled_grammar: _CompiledGrammar, - default_style: str = "", - lexers: Optional[Dict[str, Lexer]] = None, - ) -> None: - - self.compiled_grammar = compiled_grammar - self.default_style = default_style - self.lexers = lexers or {} - - def _get_text_fragments(self, text: str) -> StyleAndTextTuples: - m = self.compiled_grammar.match_prefix(text) - - if m: - characters: StyleAndTextTuples = [(self.default_style, c) for c in text] - - for v in m.variables(): - # If we have a `Lexer` instance for this part of the input. - # Tokenize recursively and apply tokens. - lexer = self.lexers.get(v.varname) - - if lexer: - document = Document(text[v.start : v.stop]) - lexer_tokens_for_line = lexer.lex_document(document) - text_fragments: StyleAndTextTuples = [] - for i in range(len(document.lines)): - text_fragments.extend(lexer_tokens_for_line(i)) - text_fragments.append(("", "\n")) - if text_fragments: - text_fragments.pop() - - i = v.start - for t, s, *_ in text_fragments: - for c in s: - if characters[i][0] == self.default_style: - characters[i] = (t, characters[i][1]) - i += 1 - - # Highlight trailing input. - trailing_input = m.trailing_input() - if trailing_input: - for i in range(trailing_input.start, trailing_input.stop): - characters[i] = ("class:trailing-input", characters[i][1]) - - return characters - else: - return [("", text)] - - def lex_document(self, document: Document) -> Callable[[int], StyleAndTextTuples]: - lines = list(split_lines(self._get_text_fragments(document.text))) - - def get_line(lineno: int) -> StyleAndTextTuples: - try: - return lines[lineno] - except IndexError: - return [] - - return get_line +""" +`GrammarLexer` is compatible with other lexers and can be used to highlight +the input using a regular grammar with annotations. +""" +from typing import Callable, Dict, Optional + +from prompt_toolkit.document import Document +from prompt_toolkit.formatted_text.base import StyleAndTextTuples +from prompt_toolkit.formatted_text.utils import split_lines +from prompt_toolkit.lexers import Lexer + +from .compiler import _CompiledGrammar + +__all__ = [ + "GrammarLexer", +] + + +class GrammarLexer(Lexer): + """ + Lexer which can be used for highlighting of fragments according to variables in the grammar. + + (It does not actual lexing of the string, but it exposes an API, compatible + with the Pygments lexer class.) + + :param compiled_grammar: Grammar as returned by the `compile()` function. + :param lexers: Dictionary mapping variable names of the regular grammar to + the lexers that should be used for this part. (This can + call other lexers recursively.) If you wish a part of the + grammar to just get one fragment, use a + `prompt_toolkit.lexers.SimpleLexer`. + """ + + def __init__( + self, + compiled_grammar: _CompiledGrammar, + default_style: str = "", + lexers: Optional[Dict[str, Lexer]] = None, + ) -> None: + + self.compiled_grammar = compiled_grammar + self.default_style = default_style + self.lexers = lexers or {} + + def _get_text_fragments(self, text: str) -> StyleAndTextTuples: + m = self.compiled_grammar.match_prefix(text) + + if m: + characters: StyleAndTextTuples = [(self.default_style, c) for c in text] + + for v in m.variables(): + # If we have a `Lexer` instance for this part of the input. + # Tokenize recursively and apply tokens. + lexer = self.lexers.get(v.varname) + + if lexer: + document = Document(text[v.start : v.stop]) + lexer_tokens_for_line = lexer.lex_document(document) + text_fragments: StyleAndTextTuples = [] + for i in range(len(document.lines)): + text_fragments.extend(lexer_tokens_for_line(i)) + text_fragments.append(("", "\n")) + if text_fragments: + text_fragments.pop() + + i = v.start + for t, s, *_ in text_fragments: + for c in s: + if characters[i][0] == self.default_style: + characters[i] = (t, characters[i][1]) + i += 1 + + # Highlight trailing input. + trailing_input = m.trailing_input() + if trailing_input: + for i in range(trailing_input.start, trailing_input.stop): + characters[i] = ("class:trailing-input", characters[i][1]) + + return characters + else: + return [("", text)] + + def lex_document(self, document: Document) -> Callable[[int], StyleAndTextTuples]: + lines = list(split_lines(self._get_text_fragments(document.text))) + + def get_line(lineno: int) -> StyleAndTextTuples: + try: + return lines[lineno] + except IndexError: + return [] + + return get_line diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/regular_languages/regex_parser.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/regular_languages/regex_parser.py index 0d9ecfe146..61d2f17a23 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/regular_languages/regex_parser.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/regular_languages/regex_parser.py @@ -1,281 +1,281 @@ -""" -Parser for parsing a regular expression. -Take a string representing a regular expression and return the root node of its -parse tree. - -usage:: - - root_node = parse_regex('(hello|world)') - -Remarks: -- The regex parser processes multiline, it ignores all whitespace and supports - multiple named groups with the same name and #-style comments. - -Limitations: -- Lookahead is not supported. -""" -import re -from typing import List, Optional - -__all__ = [ - "Repeat", - "Variable", - "Regex", - "Lookahead", - "tokenize_regex", - "parse_regex", -] - - -class Node: - """ - Base class for all the grammar nodes. - (You don't initialize this one.) - """ - - def __add__(self, other_node: "Node") -> "NodeSequence": - return NodeSequence([self, other_node]) - - def __or__(self, other_node: "Node") -> "AnyNode": - return AnyNode([self, other_node]) - - -class AnyNode(Node): - """ - Union operation (OR operation) between several grammars. You don't - initialize this yourself, but it's a result of a "Grammar1 | Grammar2" - operation. - """ - - def __init__(self, children: List[Node]) -> None: - self.children = children - - def __or__(self, other_node: Node) -> "AnyNode": - return AnyNode(self.children + [other_node]) - - def __repr__(self) -> str: - return "%s(%r)" % (self.__class__.__name__, self.children) - - -class NodeSequence(Node): - """ - Concatenation operation of several grammars. You don't initialize this - yourself, but it's a result of a "Grammar1 + Grammar2" operation. - """ - - def __init__(self, children: List[Node]) -> None: - self.children = children - - def __add__(self, other_node: Node) -> "NodeSequence": - return NodeSequence(self.children + [other_node]) - - def __repr__(self) -> str: - return "%s(%r)" % (self.__class__.__name__, self.children) - - -class Regex(Node): - """ - Regular expression. - """ - - def __init__(self, regex: str) -> None: - re.compile(regex) # Validate - - self.regex = regex - - def __repr__(self) -> str: - return "%s(/%s/)" % (self.__class__.__name__, self.regex) - - -class Lookahead(Node): - """ - Lookahead expression. - """ - - def __init__(self, childnode: Node, negative: bool = False) -> None: - self.childnode = childnode - self.negative = negative - - def __repr__(self) -> str: - return "%s(%r)" % (self.__class__.__name__, self.childnode) - - -class Variable(Node): - """ - Mark a variable in the regular grammar. This will be translated into a - named group. Each variable can have his own completer, validator, etc.. - - :param childnode: The grammar which is wrapped inside this variable. - :param varname: String. - """ - - def __init__(self, childnode: Node, varname: str = "") -> None: - self.childnode = childnode - self.varname = varname - - def __repr__(self) -> str: - return "%s(childnode=%r, varname=%r)" % ( - self.__class__.__name__, - self.childnode, - self.varname, - ) - - -class Repeat(Node): - def __init__( - self, - childnode: Node, - min_repeat: int = 0, - max_repeat: Optional[int] = None, - greedy: bool = True, - ) -> None: - self.childnode = childnode - self.min_repeat = min_repeat - self.max_repeat = max_repeat - self.greedy = greedy - - def __repr__(self) -> str: - return "%s(childnode=%r)" % (self.__class__.__name__, self.childnode) - - -def tokenize_regex(input: str) -> List[str]: - """ - Takes a string, representing a regular expression as input, and tokenizes - it. - - :param input: string, representing a regular expression. - :returns: List of tokens. - """ - # Regular expression for tokenizing other regular expressions. - p = re.compile( - r"""^( - \(\?P\<[a-zA-Z0-9_-]+\> | # Start of named group. - \(\?#[^)]*\) | # Comment - \(\?= | # Start of lookahead assertion - \(\?! | # Start of negative lookahead assertion - \(\?<= | # If preceded by. - \(\?< | # If not preceded by. - \(?: | # Start of group. (non capturing.) - \( | # Start of group. - \(?[iLmsux] | # Flags. - \(?P=[a-zA-Z]+\) | # Back reference to named group - \) | # End of group. - \{[^{}]*\} | # Repetition - \*\? | \+\? | \?\?\ | # Non greedy repetition. - \* | \+ | \? | # Repetition - \#.*\n | # Comment - \\. | - - # Character group. - \[ - ( [^\]\\] | \\.)* - \] | - - [^(){}] | - . - )""", - re.VERBOSE, - ) - - tokens = [] - - while input: - m = p.match(input) - if m: - token, input = input[: m.end()], input[m.end() :] - if not token.isspace(): - tokens.append(token) - else: - raise Exception("Could not tokenize input regex.") - - return tokens - - -def parse_regex(regex_tokens: List[str]) -> Node: - """ - Takes a list of tokens from the tokenizer, and returns a parse tree. - """ - # We add a closing brace because that represents the final pop of the stack. - tokens: List[str] = [")"] + regex_tokens[::-1] - - def wrap(lst: List[Node]) -> Node: - """Turn list into sequence when it contains several items.""" - if len(lst) == 1: - return lst[0] - else: - return NodeSequence(lst) - - def _parse() -> Node: - or_list: List[List[Node]] = [] - result: List[Node] = [] - - def wrapped_result() -> Node: - if or_list == []: - return wrap(result) - else: - or_list.append(result) - return AnyNode([wrap(i) for i in or_list]) - - while tokens: - t = tokens.pop() - - if t.startswith("(?P<"): - variable = Variable(_parse(), varname=t[4:-1]) - result.append(variable) - - elif t in ("*", "*?"): - greedy = t == "*" - result[-1] = Repeat(result[-1], greedy=greedy) - - elif t in ("+", "+?"): - greedy = t == "+" - result[-1] = Repeat(result[-1], min_repeat=1, greedy=greedy) - - elif t in ("?", "??"): - if result == []: - raise Exception("Nothing to repeat." + repr(tokens)) - else: - greedy = t == "?" - result[-1] = Repeat( - result[-1], min_repeat=0, max_repeat=1, greedy=greedy - ) - - elif t == "|": - or_list.append(result) - result = [] - - elif t in ("(", "(?:"): - result.append(_parse()) - - elif t == "(?!": - result.append(Lookahead(_parse(), negative=True)) - - elif t == "(?=": - result.append(Lookahead(_parse(), negative=False)) - - elif t == ")": - return wrapped_result() - - elif t.startswith("#"): - pass - - elif t.startswith("{"): - # TODO: implement! - raise Exception("{}-style repetition not yet supported".format(t)) - - elif t.startswith("(?"): - raise Exception("%r not supported" % t) - - elif t.isspace(): - pass - else: - result.append(Regex(t)) - - raise Exception("Expecting ')' token") - - result = _parse() - - if len(tokens) != 0: - raise Exception("Unmatched parentheses.") - else: - return result +""" +Parser for parsing a regular expression. +Take a string representing a regular expression and return the root node of its +parse tree. + +usage:: + + root_node = parse_regex('(hello|world)') + +Remarks: +- The regex parser processes multiline, it ignores all whitespace and supports + multiple named groups with the same name and #-style comments. + +Limitations: +- Lookahead is not supported. +""" +import re +from typing import List, Optional + +__all__ = [ + "Repeat", + "Variable", + "Regex", + "Lookahead", + "tokenize_regex", + "parse_regex", +] + + +class Node: + """ + Base class for all the grammar nodes. + (You don't initialize this one.) + """ + + def __add__(self, other_node: "Node") -> "NodeSequence": + return NodeSequence([self, other_node]) + + def __or__(self, other_node: "Node") -> "AnyNode": + return AnyNode([self, other_node]) + + +class AnyNode(Node): + """ + Union operation (OR operation) between several grammars. You don't + initialize this yourself, but it's a result of a "Grammar1 | Grammar2" + operation. + """ + + def __init__(self, children: List[Node]) -> None: + self.children = children + + def __or__(self, other_node: Node) -> "AnyNode": + return AnyNode(self.children + [other_node]) + + def __repr__(self) -> str: + return "%s(%r)" % (self.__class__.__name__, self.children) + + +class NodeSequence(Node): + """ + Concatenation operation of several grammars. You don't initialize this + yourself, but it's a result of a "Grammar1 + Grammar2" operation. + """ + + def __init__(self, children: List[Node]) -> None: + self.children = children + + def __add__(self, other_node: Node) -> "NodeSequence": + return NodeSequence(self.children + [other_node]) + + def __repr__(self) -> str: + return "%s(%r)" % (self.__class__.__name__, self.children) + + +class Regex(Node): + """ + Regular expression. + """ + + def __init__(self, regex: str) -> None: + re.compile(regex) # Validate + + self.regex = regex + + def __repr__(self) -> str: + return "%s(/%s/)" % (self.__class__.__name__, self.regex) + + +class Lookahead(Node): + """ + Lookahead expression. + """ + + def __init__(self, childnode: Node, negative: bool = False) -> None: + self.childnode = childnode + self.negative = negative + + def __repr__(self) -> str: + return "%s(%r)" % (self.__class__.__name__, self.childnode) + + +class Variable(Node): + """ + Mark a variable in the regular grammar. This will be translated into a + named group. Each variable can have his own completer, validator, etc.. + + :param childnode: The grammar which is wrapped inside this variable. + :param varname: String. + """ + + def __init__(self, childnode: Node, varname: str = "") -> None: + self.childnode = childnode + self.varname = varname + + def __repr__(self) -> str: + return "%s(childnode=%r, varname=%r)" % ( + self.__class__.__name__, + self.childnode, + self.varname, + ) + + +class Repeat(Node): + def __init__( + self, + childnode: Node, + min_repeat: int = 0, + max_repeat: Optional[int] = None, + greedy: bool = True, + ) -> None: + self.childnode = childnode + self.min_repeat = min_repeat + self.max_repeat = max_repeat + self.greedy = greedy + + def __repr__(self) -> str: + return "%s(childnode=%r)" % (self.__class__.__name__, self.childnode) + + +def tokenize_regex(input: str) -> List[str]: + """ + Takes a string, representing a regular expression as input, and tokenizes + it. + + :param input: string, representing a regular expression. + :returns: List of tokens. + """ + # Regular expression for tokenizing other regular expressions. + p = re.compile( + r"""^( + \(\?P\<[a-zA-Z0-9_-]+\> | # Start of named group. + \(\?#[^)]*\) | # Comment + \(\?= | # Start of lookahead assertion + \(\?! | # Start of negative lookahead assertion + \(\?<= | # If preceded by. + \(\?< | # If not preceded by. + \(?: | # Start of group. (non capturing.) + \( | # Start of group. + \(?[iLmsux] | # Flags. + \(?P=[a-zA-Z]+\) | # Back reference to named group + \) | # End of group. + \{[^{}]*\} | # Repetition + \*\? | \+\? | \?\?\ | # Non greedy repetition. + \* | \+ | \? | # Repetition + \#.*\n | # Comment + \\. | + + # Character group. + \[ + ( [^\]\\] | \\.)* + \] | + + [^(){}] | + . + )""", + re.VERBOSE, + ) + + tokens = [] + + while input: + m = p.match(input) + if m: + token, input = input[: m.end()], input[m.end() :] + if not token.isspace(): + tokens.append(token) + else: + raise Exception("Could not tokenize input regex.") + + return tokens + + +def parse_regex(regex_tokens: List[str]) -> Node: + """ + Takes a list of tokens from the tokenizer, and returns a parse tree. + """ + # We add a closing brace because that represents the final pop of the stack. + tokens: List[str] = [")"] + regex_tokens[::-1] + + def wrap(lst: List[Node]) -> Node: + """Turn list into sequence when it contains several items.""" + if len(lst) == 1: + return lst[0] + else: + return NodeSequence(lst) + + def _parse() -> Node: + or_list: List[List[Node]] = [] + result: List[Node] = [] + + def wrapped_result() -> Node: + if or_list == []: + return wrap(result) + else: + or_list.append(result) + return AnyNode([wrap(i) for i in or_list]) + + while tokens: + t = tokens.pop() + + if t.startswith("(?P<"): + variable = Variable(_parse(), varname=t[4:-1]) + result.append(variable) + + elif t in ("*", "*?"): + greedy = t == "*" + result[-1] = Repeat(result[-1], greedy=greedy) + + elif t in ("+", "+?"): + greedy = t == "+" + result[-1] = Repeat(result[-1], min_repeat=1, greedy=greedy) + + elif t in ("?", "??"): + if result == []: + raise Exception("Nothing to repeat." + repr(tokens)) + else: + greedy = t == "?" + result[-1] = Repeat( + result[-1], min_repeat=0, max_repeat=1, greedy=greedy + ) + + elif t == "|": + or_list.append(result) + result = [] + + elif t in ("(", "(?:"): + result.append(_parse()) + + elif t == "(?!": + result.append(Lookahead(_parse(), negative=True)) + + elif t == "(?=": + result.append(Lookahead(_parse(), negative=False)) + + elif t == ")": + return wrapped_result() + + elif t.startswith("#"): + pass + + elif t.startswith("{"): + # TODO: implement! + raise Exception("{}-style repetition not yet supported".format(t)) + + elif t.startswith("(?"): + raise Exception("%r not supported" % t) + + elif t.isspace(): + pass + else: + result.append(Regex(t)) + + raise Exception("Expecting ')' token") + + result = _parse() + + if len(tokens) != 0: + raise Exception("Unmatched parentheses.") + else: + return result diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/regular_languages/validation.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/regular_languages/validation.py index 9f9ca4e2fb..71d3434035 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/regular_languages/validation.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/regular_languages/validation.py @@ -1,60 +1,60 @@ -""" -Validator for a regular language. -""" -from typing import Dict - -from prompt_toolkit.document import Document -from prompt_toolkit.validation import ValidationError, Validator - -from .compiler import _CompiledGrammar - -__all__ = [ - "GrammarValidator", -] - - -class GrammarValidator(Validator): - """ - Validator which can be used for validation according to variables in - the grammar. Each variable can have its own validator. - - :param compiled_grammar: `GrammarCompleter` instance. - :param validators: `dict` mapping variable names of the grammar to the - `Validator` instances to be used for each variable. - """ - - def __init__( - self, compiled_grammar: _CompiledGrammar, validators: Dict[str, Validator] - ) -> None: - - self.compiled_grammar = compiled_grammar - self.validators = validators - - def validate(self, document: Document) -> None: - # Parse input document. - # We use `match`, not `match_prefix`, because for validation, we want - # the actual, unambiguous interpretation of the input. - m = self.compiled_grammar.match(document.text) - - if m: - for v in m.variables(): - validator = self.validators.get(v.varname) - - if validator: - # Unescape text. - unwrapped_text = self.compiled_grammar.unescape(v.varname, v.value) - - # Create a document, for the completions API (text/cursor_position) - inner_document = Document(unwrapped_text, len(unwrapped_text)) - - try: - validator.validate(inner_document) - except ValidationError as e: - raise ValidationError( - cursor_position=v.start + e.cursor_position, - message=e.message, - ) from e - else: - raise ValidationError( - cursor_position=len(document.text), message="Invalid command" - ) +""" +Validator for a regular language. +""" +from typing import Dict + +from prompt_toolkit.document import Document +from prompt_toolkit.validation import ValidationError, Validator + +from .compiler import _CompiledGrammar + +__all__ = [ + "GrammarValidator", +] + + +class GrammarValidator(Validator): + """ + Validator which can be used for validation according to variables in + the grammar. Each variable can have its own validator. + + :param compiled_grammar: `GrammarCompleter` instance. + :param validators: `dict` mapping variable names of the grammar to the + `Validator` instances to be used for each variable. + """ + + def __init__( + self, compiled_grammar: _CompiledGrammar, validators: Dict[str, Validator] + ) -> None: + + self.compiled_grammar = compiled_grammar + self.validators = validators + + def validate(self, document: Document) -> None: + # Parse input document. + # We use `match`, not `match_prefix`, because for validation, we want + # the actual, unambiguous interpretation of the input. + m = self.compiled_grammar.match(document.text) + + if m: + for v in m.variables(): + validator = self.validators.get(v.varname) + + if validator: + # Unescape text. + unwrapped_text = self.compiled_grammar.unescape(v.varname, v.value) + + # Create a document, for the completions API (text/cursor_position) + inner_document = Document(unwrapped_text, len(unwrapped_text)) + + try: + validator.validate(inner_document) + except ValidationError as e: + raise ValidationError( + cursor_position=v.start + e.cursor_position, + message=e.message, + ) from e + else: + raise ValidationError( + cursor_position=len(document.text), message="Invalid command" + ) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/ssh/__init__.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/ssh/__init__.py index 0fd836b0a9..a895ca2282 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/ssh/__init__.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/ssh/__init__.py @@ -1,6 +1,6 @@ -from .server import PromptToolkitSSHServer, PromptToolkitSSHSession - -__all__ = [ - "PromptToolkitSSHSession", - "PromptToolkitSSHServer", -] +from .server import PromptToolkitSSHServer, PromptToolkitSSHSession + +__all__ = [ + "PromptToolkitSSHSession", + "PromptToolkitSSHServer", +] diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/ssh/server.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/ssh/server.py index ca49041ee9..ba11036fc2 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/ssh/server.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/ssh/server.py @@ -1,156 +1,156 @@ -""" -Utility for running a prompt_toolkit application in an asyncssh server. -""" -import asyncio -import traceback -from typing import Any, Awaitable, Callable, Optional, TextIO, cast - -import asyncssh - -from prompt_toolkit.application.current import AppSession, create_app_session -from prompt_toolkit.data_structures import Size -from prompt_toolkit.eventloop import get_event_loop -from prompt_toolkit.input import create_pipe_input -from prompt_toolkit.output.vt100 import Vt100_Output - -__all__ = ["PromptToolkitSSHSession", "PromptToolkitSSHServer"] - - -class PromptToolkitSSHSession(asyncssh.SSHServerSession): # type: ignore - def __init__( - self, interact: Callable[["PromptToolkitSSHSession"], Awaitable[None]] - ) -> None: - self.interact = interact - self.interact_task: Optional[asyncio.Task[None]] = None - self._chan: Optional[Any] = None - self.app_session: Optional[AppSession] = None - - # PipInput object, for sending input in the CLI. - # (This is something that we can use in the prompt_toolkit event loop, - # but still write date in manually.) - self._input = create_pipe_input() - self._output: Optional[Vt100_Output] = None - - # Output object. Don't render to the real stdout, but write everything - # in the SSH channel. - class Stdout: - def write(s, data: str) -> None: - try: - if self._chan is not None: - self._chan.write(data.replace("\n", "\r\n")) - except BrokenPipeError: - pass # Channel not open for sending. - - def isatty(s) -> bool: - return True - - def flush(s) -> None: - pass - - @property - def encoding(s) -> str: - assert self._chan is not None - return str(self._chan._orig_chan.get_encoding()[0]) - - self.stdout = cast(TextIO, Stdout()) - - def _get_size(self) -> Size: - """ - Callable that returns the current `Size`, required by Vt100_Output. - """ - if self._chan is None: - return Size(rows=20, columns=79) - else: - width, height, pixwidth, pixheight = self._chan.get_terminal_size() - return Size(rows=height, columns=width) - - def connection_made(self, chan: Any) -> None: - self._chan = chan - - def shell_requested(self) -> bool: - return True - - def session_started(self) -> None: - self.interact_task = get_event_loop().create_task(self._interact()) - - async def _interact(self) -> None: - if self._chan is None: - # Should not happen. - raise Exception("`_interact` called before `connection_made`.") - - if hasattr(self._chan, "set_line_mode") and self._chan._editor is not None: - # Disable the line editing provided by asyncssh. Prompt_toolkit - # provides the line editing. - self._chan.set_line_mode(False) - - term = self._chan.get_terminal_type() - - self._output = Vt100_Output( - self.stdout, self._get_size, term=term, write_binary=False - ) - with create_app_session(input=self._input, output=self._output) as session: - self.app_session = session - try: - await self.interact(self) - except BaseException: - traceback.print_exc() - finally: - # Close the connection. - self._chan.close() - self._input.close() - - def terminal_size_changed( - self, width: int, height: int, pixwidth: object, pixheight: object - ) -> None: - # Send resize event to the current application. - if self.app_session and self.app_session.app: - self.app_session.app._on_resize() - - def data_received(self, data: str, datatype: object) -> None: - self._input.send_text(data) - - -class PromptToolkitSSHServer(asyncssh.SSHServer): # type: ignore - """ - Run a prompt_toolkit application over an asyncssh server. - - This takes one argument, an `interact` function, which is called for each - connection. This should be an asynchronous function that runs the - prompt_toolkit applications. This function runs in an `AppSession`, which - means that we can have multiple UI interactions concurrently. - - Example usage: - - .. code:: python - - async def interact(ssh_session: PromptToolkitSSHSession) -> None: - await yes_no_dialog("my title", "my text").run_async() - - prompt_session = PromptSession() - text = await prompt_session.prompt_async("Type something: ") - print_formatted_text('You said: ', text) - - server = PromptToolkitSSHServer(interact=interact) - loop = get_event_loop() - loop.run_until_complete( - asyncssh.create_server( - lambda: MySSHServer(interact), - "", - port, - server_host_keys=["/etc/ssh/..."], - ) - ) - loop.run_forever() - """ - - def __init__( - self, interact: Callable[[PromptToolkitSSHSession], Awaitable[None]] - ) -> None: - self.interact = interact - - def begin_auth(self, username: str) -> bool: - # No authentication. - return False - - def session_requested(self) -> PromptToolkitSSHSession: - return PromptToolkitSSHSession(self.interact) +""" +Utility for running a prompt_toolkit application in an asyncssh server. +""" +import asyncio +import traceback +from typing import Any, Awaitable, Callable, Optional, TextIO, cast + +import asyncssh + +from prompt_toolkit.application.current import AppSession, create_app_session +from prompt_toolkit.data_structures import Size +from prompt_toolkit.eventloop import get_event_loop +from prompt_toolkit.input import create_pipe_input +from prompt_toolkit.output.vt100 import Vt100_Output + +__all__ = ["PromptToolkitSSHSession", "PromptToolkitSSHServer"] + + +class PromptToolkitSSHSession(asyncssh.SSHServerSession): # type: ignore + def __init__( + self, interact: Callable[["PromptToolkitSSHSession"], Awaitable[None]] + ) -> None: + self.interact = interact + self.interact_task: Optional[asyncio.Task[None]] = None + self._chan: Optional[Any] = None + self.app_session: Optional[AppSession] = None + + # PipInput object, for sending input in the CLI. + # (This is something that we can use in the prompt_toolkit event loop, + # but still write date in manually.) + self._input = create_pipe_input() + self._output: Optional[Vt100_Output] = None + + # Output object. Don't render to the real stdout, but write everything + # in the SSH channel. + class Stdout: + def write(s, data: str) -> None: + try: + if self._chan is not None: + self._chan.write(data.replace("\n", "\r\n")) + except BrokenPipeError: + pass # Channel not open for sending. + + def isatty(s) -> bool: + return True + + def flush(s) -> None: + pass + + @property + def encoding(s) -> str: + assert self._chan is not None + return str(self._chan._orig_chan.get_encoding()[0]) + + self.stdout = cast(TextIO, Stdout()) + + def _get_size(self) -> Size: + """ + Callable that returns the current `Size`, required by Vt100_Output. + """ + if self._chan is None: + return Size(rows=20, columns=79) + else: + width, height, pixwidth, pixheight = self._chan.get_terminal_size() + return Size(rows=height, columns=width) + + def connection_made(self, chan: Any) -> None: + self._chan = chan + + def shell_requested(self) -> bool: + return True + + def session_started(self) -> None: + self.interact_task = get_event_loop().create_task(self._interact()) + + async def _interact(self) -> None: + if self._chan is None: + # Should not happen. + raise Exception("`_interact` called before `connection_made`.") + + if hasattr(self._chan, "set_line_mode") and self._chan._editor is not None: + # Disable the line editing provided by asyncssh. Prompt_toolkit + # provides the line editing. + self._chan.set_line_mode(False) + + term = self._chan.get_terminal_type() + + self._output = Vt100_Output( + self.stdout, self._get_size, term=term, write_binary=False + ) + with create_app_session(input=self._input, output=self._output) as session: + self.app_session = session + try: + await self.interact(self) + except BaseException: + traceback.print_exc() + finally: + # Close the connection. + self._chan.close() + self._input.close() + + def terminal_size_changed( + self, width: int, height: int, pixwidth: object, pixheight: object + ) -> None: + # Send resize event to the current application. + if self.app_session and self.app_session.app: + self.app_session.app._on_resize() + + def data_received(self, data: str, datatype: object) -> None: + self._input.send_text(data) + + +class PromptToolkitSSHServer(asyncssh.SSHServer): # type: ignore + """ + Run a prompt_toolkit application over an asyncssh server. + + This takes one argument, an `interact` function, which is called for each + connection. This should be an asynchronous function that runs the + prompt_toolkit applications. This function runs in an `AppSession`, which + means that we can have multiple UI interactions concurrently. + + Example usage: + + .. code:: python + + async def interact(ssh_session: PromptToolkitSSHSession) -> None: + await yes_no_dialog("my title", "my text").run_async() + + prompt_session = PromptSession() + text = await prompt_session.prompt_async("Type something: ") + print_formatted_text('You said: ', text) + + server = PromptToolkitSSHServer(interact=interact) + loop = get_event_loop() + loop.run_until_complete( + asyncssh.create_server( + lambda: MySSHServer(interact), + "", + port, + server_host_keys=["/etc/ssh/..."], + ) + ) + loop.run_forever() + """ + + def __init__( + self, interact: Callable[[PromptToolkitSSHSession], Awaitable[None]] + ) -> None: + self.interact = interact + + def begin_auth(self, username: str) -> bool: + # No authentication. + return False + + def session_requested(self) -> PromptToolkitSSHSession: + return PromptToolkitSSHSession(self.interact) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/telnet/__init__.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/telnet/__init__.py index 80ef666d01..b29f7d2876 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/telnet/__init__.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/telnet/__init__.py @@ -1,5 +1,5 @@ -from .server import TelnetServer - -__all__ = [ - "TelnetServer", -] +from .server import TelnetServer + +__all__ = [ + "TelnetServer", +] diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/telnet/log.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/telnet/log.py index ce9cf60bda..24487aba26 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/telnet/log.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/telnet/log.py @@ -1,10 +1,10 @@ -""" -Python logger for the telnet server. -""" -import logging - -logger = logging.getLogger(__package__) - -__all__ = [ - "logger", -] +""" +Python logger for the telnet server. +""" +import logging + +logger = logging.getLogger(__package__) + +__all__ = [ + "logger", +] diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/telnet/protocol.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/telnet/protocol.py index aa44828d82..7b6df8319f 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/telnet/protocol.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/telnet/protocol.py @@ -1,207 +1,207 @@ -""" -Parser for the Telnet protocol. (Not a complete implementation of the telnet -specification, but sufficient for a command line interface.) - -Inspired by `Twisted.conch.telnet`. -""" -import struct -from typing import Callable, Generator - -from .log import logger - -__all__ = [ - "TelnetProtocolParser", -] - - -def int2byte(number: int) -> bytes: - return bytes((number,)) - - -# Telnet constants. -NOP = int2byte(0) -SGA = int2byte(3) - -IAC = int2byte(255) -DO = int2byte(253) -DONT = int2byte(254) -LINEMODE = int2byte(34) -SB = int2byte(250) -WILL = int2byte(251) -WONT = int2byte(252) -MODE = int2byte(1) -SE = int2byte(240) -ECHO = int2byte(1) -NAWS = int2byte(31) -LINEMODE = int2byte(34) -SUPPRESS_GO_AHEAD = int2byte(3) - -TTYPE = int2byte(24) -SEND = int2byte(1) -IS = int2byte(0) - -DM = int2byte(242) -BRK = int2byte(243) -IP = int2byte(244) -AO = int2byte(245) -AYT = int2byte(246) -EC = int2byte(247) -EL = int2byte(248) -GA = int2byte(249) - - -class TelnetProtocolParser: - """ - Parser for the Telnet protocol. - Usage:: - - def data_received(data): - print(data) - - def size_received(rows, columns): - print(rows, columns) - - p = TelnetProtocolParser(data_received, size_received) - p.feed(binary_data) - """ - - def __init__( - self, - data_received_callback: Callable[[bytes], None], - size_received_callback: Callable[[int, int], None], - ttype_received_callback: Callable[[str], None], - ) -> None: - - self.data_received_callback = data_received_callback - self.size_received_callback = size_received_callback - self.ttype_received_callback = ttype_received_callback - - self._parser = self._parse_coroutine() - self._parser.send(None) # type: ignore - - def received_data(self, data: bytes) -> None: - self.data_received_callback(data) - - def do_received(self, data: bytes) -> None: - """Received telnet DO command.""" - logger.info("DO %r", data) - - def dont_received(self, data: bytes) -> None: - """Received telnet DONT command.""" - logger.info("DONT %r", data) - - def will_received(self, data: bytes) -> None: - """Received telnet WILL command.""" - logger.info("WILL %r", data) - - def wont_received(self, data: bytes) -> None: - """Received telnet WONT command.""" - logger.info("WONT %r", data) - - def command_received(self, command: bytes, data: bytes) -> None: - if command == DO: - self.do_received(data) - - elif command == DONT: - self.dont_received(data) - - elif command == WILL: - self.will_received(data) - - elif command == WONT: - self.wont_received(data) - - else: - logger.info("command received %r %r", command, data) - - def naws(self, data: bytes) -> None: - """ - Received NAWS. (Window dimensions.) - """ - if len(data) == 4: - # NOTE: the first parameter of struct.unpack should be - # a 'str' object. Both on Py2/py3. This crashes on OSX - # otherwise. - columns, rows = struct.unpack(str("!HH"), data) - self.size_received_callback(rows, columns) - else: - logger.warning("Wrong number of NAWS bytes") - - def ttype(self, data: bytes) -> None: - """ - Received terminal type. - """ - subcmd, data = data[0:1], data[1:] - if subcmd == IS: - ttype = data.decode("ascii") - self.ttype_received_callback(ttype) - else: - logger.warning("Received a non-IS terminal type Subnegotiation") - - def negotiate(self, data: bytes) -> None: - """ - Got negotiate data. - """ - command, payload = data[0:1], data[1:] - - if command == NAWS: - self.naws(payload) - elif command == TTYPE: - self.ttype(payload) - else: - logger.info("Negotiate (%r got bytes)", len(data)) - - def _parse_coroutine(self) -> Generator[None, bytes, None]: - """ - Parser state machine. - Every 'yield' expression returns the next byte. - """ - while True: - d = yield - - if d == int2byte(0): - pass # NOP - - # Go to state escaped. - elif d == IAC: - d2 = yield - - if d2 == IAC: - self.received_data(d2) - - # Handle simple commands. - elif d2 in (NOP, DM, BRK, IP, AO, AYT, EC, EL, GA): - self.command_received(d2, b"") - - # Handle IAC-[DO/DONT/WILL/WONT] commands. - elif d2 in (DO, DONT, WILL, WONT): - d3 = yield - self.command_received(d2, d3) - - # Subnegotiation - elif d2 == SB: - # Consume everything until next IAC-SE - data = [] - - while True: - d3 = yield - - if d3 == IAC: - d4 = yield - if d4 == SE: - break - else: - data.append(d4) - else: - data.append(d3) - - self.negotiate(b"".join(data)) - else: - self.received_data(d) - - def feed(self, data: bytes) -> None: - """ - Feed data to the parser. - """ - for b in data: - self._parser.send(int2byte(b)) +""" +Parser for the Telnet protocol. (Not a complete implementation of the telnet +specification, but sufficient for a command line interface.) + +Inspired by `Twisted.conch.telnet`. +""" +import struct +from typing import Callable, Generator + +from .log import logger + +__all__ = [ + "TelnetProtocolParser", +] + + +def int2byte(number: int) -> bytes: + return bytes((number,)) + + +# Telnet constants. +NOP = int2byte(0) +SGA = int2byte(3) + +IAC = int2byte(255) +DO = int2byte(253) +DONT = int2byte(254) +LINEMODE = int2byte(34) +SB = int2byte(250) +WILL = int2byte(251) +WONT = int2byte(252) +MODE = int2byte(1) +SE = int2byte(240) +ECHO = int2byte(1) +NAWS = int2byte(31) +LINEMODE = int2byte(34) +SUPPRESS_GO_AHEAD = int2byte(3) + +TTYPE = int2byte(24) +SEND = int2byte(1) +IS = int2byte(0) + +DM = int2byte(242) +BRK = int2byte(243) +IP = int2byte(244) +AO = int2byte(245) +AYT = int2byte(246) +EC = int2byte(247) +EL = int2byte(248) +GA = int2byte(249) + + +class TelnetProtocolParser: + """ + Parser for the Telnet protocol. + Usage:: + + def data_received(data): + print(data) + + def size_received(rows, columns): + print(rows, columns) + + p = TelnetProtocolParser(data_received, size_received) + p.feed(binary_data) + """ + + def __init__( + self, + data_received_callback: Callable[[bytes], None], + size_received_callback: Callable[[int, int], None], + ttype_received_callback: Callable[[str], None], + ) -> None: + + self.data_received_callback = data_received_callback + self.size_received_callback = size_received_callback + self.ttype_received_callback = ttype_received_callback + + self._parser = self._parse_coroutine() + self._parser.send(None) # type: ignore + + def received_data(self, data: bytes) -> None: + self.data_received_callback(data) + + def do_received(self, data: bytes) -> None: + """Received telnet DO command.""" + logger.info("DO %r", data) + + def dont_received(self, data: bytes) -> None: + """Received telnet DONT command.""" + logger.info("DONT %r", data) + + def will_received(self, data: bytes) -> None: + """Received telnet WILL command.""" + logger.info("WILL %r", data) + + def wont_received(self, data: bytes) -> None: + """Received telnet WONT command.""" + logger.info("WONT %r", data) + + def command_received(self, command: bytes, data: bytes) -> None: + if command == DO: + self.do_received(data) + + elif command == DONT: + self.dont_received(data) + + elif command == WILL: + self.will_received(data) + + elif command == WONT: + self.wont_received(data) + + else: + logger.info("command received %r %r", command, data) + + def naws(self, data: bytes) -> None: + """ + Received NAWS. (Window dimensions.) + """ + if len(data) == 4: + # NOTE: the first parameter of struct.unpack should be + # a 'str' object. Both on Py2/py3. This crashes on OSX + # otherwise. + columns, rows = struct.unpack(str("!HH"), data) + self.size_received_callback(rows, columns) + else: + logger.warning("Wrong number of NAWS bytes") + + def ttype(self, data: bytes) -> None: + """ + Received terminal type. + """ + subcmd, data = data[0:1], data[1:] + if subcmd == IS: + ttype = data.decode("ascii") + self.ttype_received_callback(ttype) + else: + logger.warning("Received a non-IS terminal type Subnegotiation") + + def negotiate(self, data: bytes) -> None: + """ + Got negotiate data. + """ + command, payload = data[0:1], data[1:] + + if command == NAWS: + self.naws(payload) + elif command == TTYPE: + self.ttype(payload) + else: + logger.info("Negotiate (%r got bytes)", len(data)) + + def _parse_coroutine(self) -> Generator[None, bytes, None]: + """ + Parser state machine. + Every 'yield' expression returns the next byte. + """ + while True: + d = yield + + if d == int2byte(0): + pass # NOP + + # Go to state escaped. + elif d == IAC: + d2 = yield + + if d2 == IAC: + self.received_data(d2) + + # Handle simple commands. + elif d2 in (NOP, DM, BRK, IP, AO, AYT, EC, EL, GA): + self.command_received(d2, b"") + + # Handle IAC-[DO/DONT/WILL/WONT] commands. + elif d2 in (DO, DONT, WILL, WONT): + d3 = yield + self.command_received(d2, d3) + + # Subnegotiation + elif d2 == SB: + # Consume everything until next IAC-SE + data = [] + + while True: + d3 = yield + + if d3 == IAC: + d4 = yield + if d4 == SE: + break + else: + data.append(d4) + else: + data.append(d3) + + self.negotiate(b"".join(data)) + else: + self.received_data(d) + + def feed(self, data: bytes) -> None: + """ + Feed data to the parser. + """ + for b in data: + self._parser.send(int2byte(b)) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/telnet/server.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/telnet/server.py index 3ff5ce59fd..b6e4b14065 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/telnet/server.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/contrib/telnet/server.py @@ -1,357 +1,357 @@ -""" -Telnet server. -""" -import asyncio -import socket +""" +Telnet server. +""" +import asyncio +import socket import sys from typing import Any, Awaitable, Callable, List, Optional, Set, TextIO, Tuple, cast - -from prompt_toolkit.application.current import create_app_session, get_app -from prompt_toolkit.application.run_in_terminal import run_in_terminal -from prompt_toolkit.data_structures import Size -from prompt_toolkit.eventloop import get_event_loop -from prompt_toolkit.formatted_text import AnyFormattedText, to_formatted_text -from prompt_toolkit.input import create_pipe_input -from prompt_toolkit.output.vt100 import Vt100_Output -from prompt_toolkit.renderer import print_formatted_text as print_formatted_text -from prompt_toolkit.styles import BaseStyle, DummyStyle - -from .log import logger -from .protocol import ( - DO, - ECHO, - IAC, - LINEMODE, - MODE, - NAWS, - SB, - SE, - SEND, - SUPPRESS_GO_AHEAD, - TTYPE, - WILL, - TelnetProtocolParser, -) - + +from prompt_toolkit.application.current import create_app_session, get_app +from prompt_toolkit.application.run_in_terminal import run_in_terminal +from prompt_toolkit.data_structures import Size +from prompt_toolkit.eventloop import get_event_loop +from prompt_toolkit.formatted_text import AnyFormattedText, to_formatted_text +from prompt_toolkit.input import create_pipe_input +from prompt_toolkit.output.vt100 import Vt100_Output +from prompt_toolkit.renderer import print_formatted_text as print_formatted_text +from prompt_toolkit.styles import BaseStyle, DummyStyle + +from .log import logger +from .protocol import ( + DO, + ECHO, + IAC, + LINEMODE, + MODE, + NAWS, + SB, + SE, + SEND, + SUPPRESS_GO_AHEAD, + TTYPE, + WILL, + TelnetProtocolParser, +) + if sys.version_info >= (3, 7): import contextvars # Requires Python3.7! else: contextvars: Any = None -__all__ = [ - "TelnetServer", -] - - -def int2byte(number: int) -> bytes: - return bytes((number,)) - - -def _initialize_telnet(connection: socket.socket) -> None: - logger.info("Initializing telnet connection") - - # Iac Do Linemode - connection.send(IAC + DO + LINEMODE) - - # Suppress Go Ahead. (This seems important for Putty to do correct echoing.) - # This will allow bi-directional operation. - connection.send(IAC + WILL + SUPPRESS_GO_AHEAD) - - # Iac sb - connection.send(IAC + SB + LINEMODE + MODE + int2byte(0) + IAC + SE) - - # IAC Will Echo - connection.send(IAC + WILL + ECHO) - - # Negotiate window size - connection.send(IAC + DO + NAWS) - - # Negotiate terminal type - # Assume the client will accept the negociation with `IAC + WILL + TTYPE` - connection.send(IAC + DO + TTYPE) - - # We can then select the first terminal type supported by the client, - # which is generally the best type the client supports - # The client should reply with a `IAC + SB + TTYPE + IS + ttype + IAC + SE` - connection.send(IAC + SB + TTYPE + SEND + IAC + SE) - - -class _ConnectionStdout: - """ - Wrapper around socket which provides `write` and `flush` methods for the - Vt100_Output output. - """ - - def __init__(self, connection: socket.socket, encoding: str) -> None: - self._encoding = encoding - self._connection = connection - self._errors = "strict" - self._buffer: List[bytes] = [] - - def write(self, data: str) -> None: - data = data.replace("\n", "\r\n") - self._buffer.append(data.encode(self._encoding, errors=self._errors)) - self.flush() - - def isatty(self) -> bool: - return True - - def flush(self) -> None: - try: - self._connection.send(b"".join(self._buffer)) - except socket.error as e: - logger.warning("Couldn't send data over socket: %s" % e) - - self._buffer = [] - - @property - def encoding(self) -> str: - return self._encoding - - @property - def errors(self) -> str: - return self._errors - - -class TelnetConnection: - """ - Class that represents one Telnet connection. - """ - - def __init__( - self, - conn: socket.socket, - addr: Tuple[str, int], - interact: Callable[["TelnetConnection"], Awaitable[None]], - server: "TelnetServer", - encoding: str, - style: Optional[BaseStyle], - ) -> None: - - self.conn = conn - self.addr = addr - self.interact = interact - self.server = server - self.encoding = encoding - self.style = style - self._closed = False - self._ready = asyncio.Event() - self.vt100_output = None - - # Create "Output" object. - self.size = Size(rows=40, columns=79) - - # Initialize. - _initialize_telnet(conn) - - # Create input. - self.vt100_input = create_pipe_input() - - # Create output. - def get_size() -> Size: - return self.size - - self.stdout = cast(TextIO, _ConnectionStdout(conn, encoding=encoding)) - - def data_received(data: bytes) -> None: - """TelnetProtocolParser 'data_received' callback""" - self.vt100_input.send_bytes(data) - - def size_received(rows: int, columns: int) -> None: - """TelnetProtocolParser 'size_received' callback""" - self.size = Size(rows=rows, columns=columns) - if self.vt100_output is not None: - get_app()._on_resize() - - def ttype_received(ttype: str) -> None: - """TelnetProtocolParser 'ttype_received' callback""" - self.vt100_output = Vt100_Output( - self.stdout, get_size, term=ttype, write_binary=False - ) - self._ready.set() - - self.parser = TelnetProtocolParser(data_received, size_received, ttype_received) - self.context: Optional[contextvars.Context] = None - - async def run_application(self) -> None: - """ - Run application. - """ - - def handle_incoming_data() -> None: - data = self.conn.recv(1024) - if data: - self.feed(data) - else: - # Connection closed by client. - logger.info("Connection closed by client. %r %r" % self.addr) - self.close() - - # Add reader. - loop = get_event_loop() - loop.add_reader(self.conn, handle_incoming_data) - - try: - # Wait for v100_output to be properly instantiated - await self._ready.wait() - with create_app_session(input=self.vt100_input, output=self.vt100_output): - self.context = contextvars.copy_context() - await self.interact(self) - except Exception as e: - print("Got %s" % type(e).__name__, e) - import traceback - - traceback.print_exc() - raise - finally: - self.close() - - def feed(self, data: bytes) -> None: - """ - Handler for incoming data. (Called by TelnetServer.) - """ - self.parser.feed(data) - - def close(self) -> None: - """ - Closed by client. - """ - if not self._closed: - self._closed = True - - self.vt100_input.close() - get_event_loop().remove_reader(self.conn) - self.conn.close() - - def send(self, formatted_text: AnyFormattedText) -> None: - """ - Send text to the client. - """ - if self.vt100_output is None: - return - formatted_text = to_formatted_text(formatted_text) - print_formatted_text( - self.vt100_output, formatted_text, self.style or DummyStyle() - ) - - def send_above_prompt(self, formatted_text: AnyFormattedText) -> None: - """ - Send text to the client. - This is asynchronous, returns a `Future`. - """ - formatted_text = to_formatted_text(formatted_text) - return self._run_in_terminal(lambda: self.send(formatted_text)) - - def _run_in_terminal(self, func: Callable[[], None]) -> None: - # Make sure that when an application was active for this connection, - # that we print the text above the application. - if self.context: - self.context.run(run_in_terminal, func) - else: - raise RuntimeError("Called _run_in_terminal outside `run_application`.") - - def erase_screen(self) -> None: - """ - Erase the screen and move the cursor to the top. - """ - if self.vt100_output is None: - return - self.vt100_output.erase_screen() - self.vt100_output.cursor_goto(0, 0) - self.vt100_output.flush() - - -async def _dummy_interact(connection: TelnetConnection) -> None: - pass - - -class TelnetServer: - """ - Telnet server implementation. - """ - - def __init__( - self, - host: str = "127.0.0.1", - port: int = 23, - interact: Callable[[TelnetConnection], Awaitable[None]] = _dummy_interact, - encoding: str = "utf-8", - style: Optional[BaseStyle] = None, - ) -> None: - - self.host = host - self.port = port - self.interact = interact - self.encoding = encoding - self.style = style - self._application_tasks: List[asyncio.Task[None]] = [] - - self.connections: Set[TelnetConnection] = set() - self._listen_socket: Optional[socket.socket] = None - - @classmethod - def _create_socket(cls, host: str, port: int) -> socket.socket: - # Create and bind socket - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - s.bind((host, port)) - - s.listen(4) - return s - - def start(self) -> None: - """ - Start the telnet server. - Don't forget to call `loop.run_forever()` after doing this. - """ - self._listen_socket = self._create_socket(self.host, self.port) - logger.info( - "Listening for telnet connections on %s port %r", self.host, self.port - ) - - get_event_loop().add_reader(self._listen_socket, self._accept) - - async def stop(self) -> None: - if self._listen_socket: - get_event_loop().remove_reader(self._listen_socket) - self._listen_socket.close() - - # Wait for all applications to finish. - for t in self._application_tasks: - t.cancel() - - for t in self._application_tasks: +__all__ = [ + "TelnetServer", +] + + +def int2byte(number: int) -> bytes: + return bytes((number,)) + + +def _initialize_telnet(connection: socket.socket) -> None: + logger.info("Initializing telnet connection") + + # Iac Do Linemode + connection.send(IAC + DO + LINEMODE) + + # Suppress Go Ahead. (This seems important for Putty to do correct echoing.) + # This will allow bi-directional operation. + connection.send(IAC + WILL + SUPPRESS_GO_AHEAD) + + # Iac sb + connection.send(IAC + SB + LINEMODE + MODE + int2byte(0) + IAC + SE) + + # IAC Will Echo + connection.send(IAC + WILL + ECHO) + + # Negotiate window size + connection.send(IAC + DO + NAWS) + + # Negotiate terminal type + # Assume the client will accept the negociation with `IAC + WILL + TTYPE` + connection.send(IAC + DO + TTYPE) + + # We can then select the first terminal type supported by the client, + # which is generally the best type the client supports + # The client should reply with a `IAC + SB + TTYPE + IS + ttype + IAC + SE` + connection.send(IAC + SB + TTYPE + SEND + IAC + SE) + + +class _ConnectionStdout: + """ + Wrapper around socket which provides `write` and `flush` methods for the + Vt100_Output output. + """ + + def __init__(self, connection: socket.socket, encoding: str) -> None: + self._encoding = encoding + self._connection = connection + self._errors = "strict" + self._buffer: List[bytes] = [] + + def write(self, data: str) -> None: + data = data.replace("\n", "\r\n") + self._buffer.append(data.encode(self._encoding, errors=self._errors)) + self.flush() + + def isatty(self) -> bool: + return True + + def flush(self) -> None: + try: + self._connection.send(b"".join(self._buffer)) + except socket.error as e: + logger.warning("Couldn't send data over socket: %s" % e) + + self._buffer = [] + + @property + def encoding(self) -> str: + return self._encoding + + @property + def errors(self) -> str: + return self._errors + + +class TelnetConnection: + """ + Class that represents one Telnet connection. + """ + + def __init__( + self, + conn: socket.socket, + addr: Tuple[str, int], + interact: Callable[["TelnetConnection"], Awaitable[None]], + server: "TelnetServer", + encoding: str, + style: Optional[BaseStyle], + ) -> None: + + self.conn = conn + self.addr = addr + self.interact = interact + self.server = server + self.encoding = encoding + self.style = style + self._closed = False + self._ready = asyncio.Event() + self.vt100_output = None + + # Create "Output" object. + self.size = Size(rows=40, columns=79) + + # Initialize. + _initialize_telnet(conn) + + # Create input. + self.vt100_input = create_pipe_input() + + # Create output. + def get_size() -> Size: + return self.size + + self.stdout = cast(TextIO, _ConnectionStdout(conn, encoding=encoding)) + + def data_received(data: bytes) -> None: + """TelnetProtocolParser 'data_received' callback""" + self.vt100_input.send_bytes(data) + + def size_received(rows: int, columns: int) -> None: + """TelnetProtocolParser 'size_received' callback""" + self.size = Size(rows=rows, columns=columns) + if self.vt100_output is not None: + get_app()._on_resize() + + def ttype_received(ttype: str) -> None: + """TelnetProtocolParser 'ttype_received' callback""" + self.vt100_output = Vt100_Output( + self.stdout, get_size, term=ttype, write_binary=False + ) + self._ready.set() + + self.parser = TelnetProtocolParser(data_received, size_received, ttype_received) + self.context: Optional[contextvars.Context] = None + + async def run_application(self) -> None: + """ + Run application. + """ + + def handle_incoming_data() -> None: + data = self.conn.recv(1024) + if data: + self.feed(data) + else: + # Connection closed by client. + logger.info("Connection closed by client. %r %r" % self.addr) + self.close() + + # Add reader. + loop = get_event_loop() + loop.add_reader(self.conn, handle_incoming_data) + + try: + # Wait for v100_output to be properly instantiated + await self._ready.wait() + with create_app_session(input=self.vt100_input, output=self.vt100_output): + self.context = contextvars.copy_context() + await self.interact(self) + except Exception as e: + print("Got %s" % type(e).__name__, e) + import traceback + + traceback.print_exc() + raise + finally: + self.close() + + def feed(self, data: bytes) -> None: + """ + Handler for incoming data. (Called by TelnetServer.) + """ + self.parser.feed(data) + + def close(self) -> None: + """ + Closed by client. + """ + if not self._closed: + self._closed = True + + self.vt100_input.close() + get_event_loop().remove_reader(self.conn) + self.conn.close() + + def send(self, formatted_text: AnyFormattedText) -> None: + """ + Send text to the client. + """ + if self.vt100_output is None: + return + formatted_text = to_formatted_text(formatted_text) + print_formatted_text( + self.vt100_output, formatted_text, self.style or DummyStyle() + ) + + def send_above_prompt(self, formatted_text: AnyFormattedText) -> None: + """ + Send text to the client. + This is asynchronous, returns a `Future`. + """ + formatted_text = to_formatted_text(formatted_text) + return self._run_in_terminal(lambda: self.send(formatted_text)) + + def _run_in_terminal(self, func: Callable[[], None]) -> None: + # Make sure that when an application was active for this connection, + # that we print the text above the application. + if self.context: + self.context.run(run_in_terminal, func) + else: + raise RuntimeError("Called _run_in_terminal outside `run_application`.") + + def erase_screen(self) -> None: + """ + Erase the screen and move the cursor to the top. + """ + if self.vt100_output is None: + return + self.vt100_output.erase_screen() + self.vt100_output.cursor_goto(0, 0) + self.vt100_output.flush() + + +async def _dummy_interact(connection: TelnetConnection) -> None: + pass + + +class TelnetServer: + """ + Telnet server implementation. + """ + + def __init__( + self, + host: str = "127.0.0.1", + port: int = 23, + interact: Callable[[TelnetConnection], Awaitable[None]] = _dummy_interact, + encoding: str = "utf-8", + style: Optional[BaseStyle] = None, + ) -> None: + + self.host = host + self.port = port + self.interact = interact + self.encoding = encoding + self.style = style + self._application_tasks: List[asyncio.Task[None]] = [] + + self.connections: Set[TelnetConnection] = set() + self._listen_socket: Optional[socket.socket] = None + + @classmethod + def _create_socket(cls, host: str, port: int) -> socket.socket: + # Create and bind socket + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind((host, port)) + + s.listen(4) + return s + + def start(self) -> None: + """ + Start the telnet server. + Don't forget to call `loop.run_forever()` after doing this. + """ + self._listen_socket = self._create_socket(self.host, self.port) + logger.info( + "Listening for telnet connections on %s port %r", self.host, self.port + ) + + get_event_loop().add_reader(self._listen_socket, self._accept) + + async def stop(self) -> None: + if self._listen_socket: + get_event_loop().remove_reader(self._listen_socket) + self._listen_socket.close() + + # Wait for all applications to finish. + for t in self._application_tasks: + t.cancel() + + for t in self._application_tasks: try: await t except asyncio.CancelledError: logger.debug("Task %s cancelled", str(t)) - - def _accept(self) -> None: - """ - Accept new incoming connection. - """ - if self._listen_socket is None: - return # Should not happen. `_accept` is called after `start`. - - conn, addr = self._listen_socket.accept() - logger.info("New connection %r %r", *addr) - - connection = TelnetConnection( - conn, addr, self.interact, self, encoding=self.encoding, style=self.style - ) - self.connections.add(connection) - - # Run application for this connection. - async def run() -> None: - logger.info("Starting interaction %r %r", *addr) - try: - await connection.run_application() - except Exception as e: - print(e) - finally: - self.connections.remove(connection) - self._application_tasks.remove(task) - logger.info("Stopping interaction %r %r", *addr) - - task = get_event_loop().create_task(run()) - self._application_tasks.append(task) + + def _accept(self) -> None: + """ + Accept new incoming connection. + """ + if self._listen_socket is None: + return # Should not happen. `_accept` is called after `start`. + + conn, addr = self._listen_socket.accept() + logger.info("New connection %r %r", *addr) + + connection = TelnetConnection( + conn, addr, self.interact, self, encoding=self.encoding, style=self.style + ) + self.connections.add(connection) + + # Run application for this connection. + async def run() -> None: + logger.info("Starting interaction %r %r", *addr) + try: + await connection.run_application() + except Exception as e: + print(e) + finally: + self.connections.remove(connection) + self._application_tasks.remove(task) + logger.info("Stopping interaction %r %r", *addr) + + task = get_event_loop().create_task(run()) + self._application_tasks.append(task) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/data_structures.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/data_structures.py index c6771100d8..454bb964ec 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/data_structures.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/data_structures.py @@ -1,10 +1,10 @@ -from typing import NamedTuple - -__all__ = [ - "Point", - "Size", -] - - -Point = NamedTuple("Point", [("x", int), ("y", int)]) -Size = NamedTuple("Size", [("rows", int), ("columns", int)]) +from typing import NamedTuple + +__all__ = [ + "Point", + "Size", +] + + +Point = NamedTuple("Point", [("x", int), ("y", int)]) +Size = NamedTuple("Size", [("rows", int), ("columns", int)]) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/document.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/document.py index 671fcd66e4..f5f24b37b5 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/document.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/document.py @@ -1,1190 +1,1190 @@ -""" -The `Document` that implements all the text operations/querying. -""" -import bisect -import re -import string -import weakref -from typing import ( - Callable, - Dict, - Iterable, - List, - NoReturn, - Optional, - Pattern, - Tuple, - cast, -) - -from .clipboard import ClipboardData -from .filters import vi_mode -from .selection import PasteMode, SelectionState, SelectionType - -__all__ = [ - "Document", -] - - -# Regex for finding "words" in documents. (We consider a group of alnum -# characters a word, but also a group of special characters a word, as long as -# it doesn't contain a space.) -# (This is a 'word' in Vi.) -_FIND_WORD_RE = re.compile(r"([a-zA-Z0-9_]+|[^a-zA-Z0-9_\s]+)") -_FIND_CURRENT_WORD_RE = re.compile(r"^([a-zA-Z0-9_]+|[^a-zA-Z0-9_\s]+)") -_FIND_CURRENT_WORD_INCLUDE_TRAILING_WHITESPACE_RE = re.compile( - r"^(([a-zA-Z0-9_]+|[^a-zA-Z0-9_\s]+)\s*)" -) - -# Regex for finding "WORDS" in documents. -# (This is a 'WORD in Vi.) -_FIND_BIG_WORD_RE = re.compile(r"([^\s]+)") -_FIND_CURRENT_BIG_WORD_RE = re.compile(r"^([^\s]+)") -_FIND_CURRENT_BIG_WORD_INCLUDE_TRAILING_WHITESPACE_RE = re.compile(r"^([^\s]+\s*)") - -# Share the Document._cache between all Document instances. -# (Document instances are considered immutable. That means that if another -# `Document` is constructed with the same text, it should have the same -# `_DocumentCache`.) -_text_to_document_cache: Dict[str, "_DocumentCache"] = cast( - Dict[str, "_DocumentCache"], - weakref.WeakValueDictionary(), # Maps document.text to DocumentCache instance. -) - - -class _ImmutableLineList(List[str]): - """ - Some protection for our 'lines' list, which is assumed to be immutable in the cache. - (Useful for detecting obvious bugs.) - """ - - def _error(self, *a: object, **kw: object) -> NoReturn: - raise NotImplementedError("Attempt to modify an immutable list.") - - __setitem__ = _error # type: ignore - append = _error - clear = _error - extend = _error - insert = _error - pop = _error - remove = _error - reverse = _error - sort = _error # type: ignore - - -class _DocumentCache: - def __init__(self) -> None: - #: List of lines for the Document text. - self.lines: Optional[_ImmutableLineList] = None - - #: List of index positions, pointing to the start of all the lines. - self.line_indexes: Optional[List[int]] = None - - -class Document: - """ - This is a immutable class around the text and cursor position, and contains - methods for querying this data, e.g. to give the text before the cursor. - - This class is usually instantiated by a :class:`~prompt_toolkit.buffer.Buffer` - object, and accessed as the `document` property of that class. - - :param text: string - :param cursor_position: int - :param selection: :class:`.SelectionState` - """ - - __slots__ = ("_text", "_cursor_position", "_selection", "_cache") - - def __init__( - self, - text: str = "", - cursor_position: Optional[int] = None, - selection: Optional[SelectionState] = None, - ) -> None: - - # Check cursor position. It can also be right after the end. (Where we - # insert text.) - assert cursor_position is None or cursor_position <= len(text), AssertionError( - "cursor_position=%r, len_text=%r" % (cursor_position, len(text)) - ) - - # By default, if no cursor position was given, make sure to put the - # cursor position is at the end of the document. This is what makes - # sense in most places. - if cursor_position is None: - cursor_position = len(text) - - # Keep these attributes private. A `Document` really has to be - # considered to be immutable, because otherwise the caching will break - # things. Because of that, we wrap these into read-only properties. - self._text = text - self._cursor_position = cursor_position - self._selection = selection - - # Cache for lines/indexes. (Shared with other Document instances that - # contain the same text. - try: - self._cache = _text_to_document_cache[self.text] - except KeyError: - self._cache = _DocumentCache() - _text_to_document_cache[self.text] = self._cache - - # XX: For some reason, above, we can't use 'WeakValueDictionary.setdefault'. - # This fails in Pypy3. `self._cache` becomes None, because that's what - # 'setdefault' returns. - # self._cache = _text_to_document_cache.setdefault(self.text, _DocumentCache()) - # assert self._cache - - def __repr__(self) -> str: - return "%s(%r, %r)" % (self.__class__.__name__, self.text, self.cursor_position) - - def __eq__(self, other: object) -> bool: - if not isinstance(other, Document): - return False - - return ( - self.text == other.text - and self.cursor_position == other.cursor_position - and self.selection == other.selection - ) - - @property - def text(self) -> str: - "The document text." - return self._text - - @property - def cursor_position(self) -> int: - "The document cursor position." - return self._cursor_position - - @property - def selection(self) -> Optional[SelectionState]: - ":class:`.SelectionState` object." - return self._selection - - @property - def current_char(self) -> str: - """Return character under cursor or an empty string.""" - return self._get_char_relative_to_cursor(0) or "" - - @property - def char_before_cursor(self) -> str: - """Return character before the cursor or an empty string.""" - return self._get_char_relative_to_cursor(-1) or "" - - @property - def text_before_cursor(self) -> str: - return self.text[: self.cursor_position :] - - @property - def text_after_cursor(self) -> str: - return self.text[self.cursor_position :] - - @property - def current_line_before_cursor(self) -> str: - """Text from the start of the line until the cursor.""" - _, _, text = self.text_before_cursor.rpartition("\n") - return text - - @property - def current_line_after_cursor(self) -> str: - """Text from the cursor until the end of the line.""" - text, _, _ = self.text_after_cursor.partition("\n") - return text - - @property - def lines(self) -> List[str]: - """ - Array of all the lines. - """ - # Cache, because this one is reused very often. - if self._cache.lines is None: - self._cache.lines = _ImmutableLineList(self.text.split("\n")) - - return self._cache.lines - - @property - def _line_start_indexes(self) -> List[int]: - """ - Array pointing to the start indexes of all the lines. - """ - # Cache, because this is often reused. (If it is used, it's often used - # many times. And this has to be fast for editing big documents!) - if self._cache.line_indexes is None: - # Create list of line lengths. - line_lengths = map(len, self.lines) - - # Calculate cumulative sums. - indexes = [0] - append = indexes.append - pos = 0 - - for line_length in line_lengths: - pos += line_length + 1 - append(pos) - - # Remove the last item. (This is not a new line.) - if len(indexes) > 1: - indexes.pop() - - self._cache.line_indexes = indexes - - return self._cache.line_indexes - - @property - def lines_from_current(self) -> List[str]: - """ - Array of the lines starting from the current line, until the last line. - """ - return self.lines[self.cursor_position_row :] - - @property - def line_count(self) -> int: - r"""Return the number of lines in this document. If the document ends - with a trailing \n, that counts as the beginning of a new line.""" - return len(self.lines) - - @property - def current_line(self) -> str: - """Return the text on the line where the cursor is. (when the input - consists of just one line, it equals `text`.""" - return self.current_line_before_cursor + self.current_line_after_cursor - - @property - def leading_whitespace_in_current_line(self) -> str: - """The leading whitespace in the left margin of the current line.""" - current_line = self.current_line - length = len(current_line) - len(current_line.lstrip()) - return current_line[:length] - - def _get_char_relative_to_cursor(self, offset: int = 0) -> str: - """ - Return character relative to cursor position, or empty string - """ - try: - return self.text[self.cursor_position + offset] - except IndexError: - return "" - - @property - def on_first_line(self) -> bool: - """ - True when we are at the first line. - """ - return self.cursor_position_row == 0 - - @property - def on_last_line(self) -> bool: - """ - True when we are at the last line. - """ - return self.cursor_position_row == self.line_count - 1 - - @property - def cursor_position_row(self) -> int: - """ - Current row. (0-based.) - """ - row, _ = self._find_line_start_index(self.cursor_position) - return row - - @property - def cursor_position_col(self) -> int: - """ - Current column. (0-based.) - """ - # (Don't use self.text_before_cursor to calculate this. Creating - # substrings and doing rsplit is too expensive for getting the cursor - # position.) - _, line_start_index = self._find_line_start_index(self.cursor_position) - return self.cursor_position - line_start_index - - def _find_line_start_index(self, index: int) -> Tuple[int, int]: - """ - For the index of a character at a certain line, calculate the index of - the first character on that line. - - Return (row, index) tuple. - """ - indexes = self._line_start_indexes - - pos = bisect.bisect_right(indexes, index) - 1 - return pos, indexes[pos] - - def translate_index_to_position(self, index: int) -> Tuple[int, int]: - """ - Given an index for the text, return the corresponding (row, col) tuple. - (0-based. Returns (0, 0) for index=0.) - """ - # Find start of this line. - row, row_index = self._find_line_start_index(index) - col = index - row_index - - return row, col - - def translate_row_col_to_index(self, row: int, col: int) -> int: - """ - Given a (row, col) tuple, return the corresponding index. - (Row and col params are 0-based.) - - Negative row/col values are turned into zero. - """ - try: - result = self._line_start_indexes[row] - line = self.lines[row] - except IndexError: - if row < 0: - result = self._line_start_indexes[0] - line = self.lines[0] - else: - result = self._line_start_indexes[-1] - line = self.lines[-1] - - result += max(0, min(col, len(line))) - - # Keep in range. (len(self.text) is included, because the cursor can be - # right after the end of the text as well.) - result = max(0, min(result, len(self.text))) - return result - - @property - def is_cursor_at_the_end(self) -> bool: - """True when the cursor is at the end of the text.""" - return self.cursor_position == len(self.text) - - @property - def is_cursor_at_the_end_of_line(self) -> bool: - """True when the cursor is at the end of this line.""" - return self.current_char in ("\n", "") - - def has_match_at_current_position(self, sub: str) -> bool: - """ - `True` when this substring is found at the cursor position. - """ - return self.text.find(sub, self.cursor_position) == self.cursor_position - - def find( - self, - sub: str, - in_current_line: bool = False, - include_current_position: bool = False, - ignore_case: bool = False, - count: int = 1, - ) -> Optional[int]: - """ - Find `text` after the cursor, return position relative to the cursor - position. Return `None` if nothing was found. - - :param count: Find the n-th occurrence. - """ - assert isinstance(ignore_case, bool) - - if in_current_line: - text = self.current_line_after_cursor - else: - text = self.text_after_cursor - - if not include_current_position: - if len(text) == 0: - return None # (Otherwise, we always get a match for the empty string.) - else: - text = text[1:] - - flags = re.IGNORECASE if ignore_case else 0 - iterator = re.finditer(re.escape(sub), text, flags) - - try: - for i, match in enumerate(iterator): - if i + 1 == count: - if include_current_position: - return match.start(0) - else: - return match.start(0) + 1 - except StopIteration: - pass - return None - - def find_all(self, sub: str, ignore_case: bool = False) -> List[int]: - """ - Find all occurrences of the substring. Return a list of absolute - positions in the document. - """ - flags = re.IGNORECASE if ignore_case else 0 - return [a.start() for a in re.finditer(re.escape(sub), self.text, flags)] - - def find_backwards( - self, - sub: str, - in_current_line: bool = False, - ignore_case: bool = False, - count: int = 1, - ) -> Optional[int]: - """ - Find `text` before the cursor, return position relative to the cursor - position. Return `None` if nothing was found. - - :param count: Find the n-th occurrence. - """ - if in_current_line: - before_cursor = self.current_line_before_cursor[::-1] - else: - before_cursor = self.text_before_cursor[::-1] - - flags = re.IGNORECASE if ignore_case else 0 - iterator = re.finditer(re.escape(sub[::-1]), before_cursor, flags) - - try: - for i, match in enumerate(iterator): - if i + 1 == count: - return -match.start(0) - len(sub) - except StopIteration: - pass - return None - - def get_word_before_cursor( - self, WORD: bool = False, pattern: Optional[Pattern[str]] = None - ) -> str: - """ - Give the word before the cursor. - If we have whitespace before the cursor this returns an empty string. - - :param pattern: (None or compiled regex). When given, use this regex - pattern. - """ - if self._is_word_before_cursor_complete(WORD=WORD, pattern=pattern): - # Space before the cursor or no text before cursor. - return "" - - text_before_cursor = self.text_before_cursor - start = self.find_start_of_previous_word(WORD=WORD, pattern=pattern) or 0 - - return text_before_cursor[len(text_before_cursor) + start :] - - def _is_word_before_cursor_complete( - self, WORD: bool = False, pattern: Optional[Pattern[str]] = None - ) -> bool: - if pattern: - return self.find_start_of_previous_word(WORD=WORD, pattern=pattern) is None - else: - return ( - self.text_before_cursor == "" or self.text_before_cursor[-1:].isspace() - ) - - def find_start_of_previous_word( - self, count: int = 1, WORD: bool = False, pattern: Optional[Pattern[str]] = None - ) -> Optional[int]: - """ - Return an index relative to the cursor position pointing to the start - of the previous word. Return `None` if nothing was found. - - :param pattern: (None or compiled regex). When given, use this regex - pattern. - """ - assert not (WORD and pattern) - - # Reverse the text before the cursor, in order to do an efficient - # backwards search. - text_before_cursor = self.text_before_cursor[::-1] - - if pattern: - regex = pattern - elif WORD: - regex = _FIND_BIG_WORD_RE - else: - regex = _FIND_WORD_RE - - iterator = regex.finditer(text_before_cursor) - - try: - for i, match in enumerate(iterator): - if i + 1 == count: - return -match.end(0) - except StopIteration: - pass - return None - - def find_boundaries_of_current_word( - self, - WORD: bool = False, - include_leading_whitespace: bool = False, - include_trailing_whitespace: bool = False, - ) -> Tuple[int, int]: - """ - Return the relative boundaries (startpos, endpos) of the current word under the - cursor. (This is at the current line, because line boundaries obviously - don't belong to any word.) - If not on a word, this returns (0,0) - """ - text_before_cursor = self.current_line_before_cursor[::-1] - text_after_cursor = self.current_line_after_cursor - - def get_regex(include_whitespace: bool) -> Pattern[str]: - return { - (False, False): _FIND_CURRENT_WORD_RE, - (False, True): _FIND_CURRENT_WORD_INCLUDE_TRAILING_WHITESPACE_RE, - (True, False): _FIND_CURRENT_BIG_WORD_RE, - (True, True): _FIND_CURRENT_BIG_WORD_INCLUDE_TRAILING_WHITESPACE_RE, - }[(WORD, include_whitespace)] - - match_before = get_regex(include_leading_whitespace).search(text_before_cursor) - match_after = get_regex(include_trailing_whitespace).search(text_after_cursor) - - # When there is a match before and after, and we're not looking for - # WORDs, make sure that both the part before and after the cursor are - # either in the [a-zA-Z_] alphabet or not. Otherwise, drop the part - # before the cursor. - if not WORD and match_before and match_after: - c1 = self.text[self.cursor_position - 1] - c2 = self.text[self.cursor_position] - alphabet = string.ascii_letters + "0123456789_" - - if (c1 in alphabet) != (c2 in alphabet): - match_before = None - - return ( - -match_before.end(1) if match_before else 0, - match_after.end(1) if match_after else 0, - ) - - def get_word_under_cursor(self, WORD: bool = False) -> str: - """ - Return the word, currently below the cursor. - This returns an empty string when the cursor is on a whitespace region. - """ - start, end = self.find_boundaries_of_current_word(WORD=WORD) - return self.text[self.cursor_position + start : self.cursor_position + end] - - def find_next_word_beginning( - self, count: int = 1, WORD: bool = False - ) -> Optional[int]: - """ - Return an index relative to the cursor position pointing to the start - of the next word. Return `None` if nothing was found. - """ - if count < 0: - return self.find_previous_word_beginning(count=-count, WORD=WORD) - - regex = _FIND_BIG_WORD_RE if WORD else _FIND_WORD_RE - iterator = regex.finditer(self.text_after_cursor) - - try: - for i, match in enumerate(iterator): - # Take first match, unless it's the word on which we're right now. - if i == 0 and match.start(1) == 0: - count += 1 - - if i + 1 == count: - return match.start(1) - except StopIteration: - pass - return None - - def find_next_word_ending( - self, include_current_position: bool = False, count: int = 1, WORD: bool = False - ) -> Optional[int]: - """ - Return an index relative to the cursor position pointing to the end - of the next word. Return `None` if nothing was found. - """ - if count < 0: - return self.find_previous_word_ending(count=-count, WORD=WORD) - - if include_current_position: - text = self.text_after_cursor - else: - text = self.text_after_cursor[1:] - - regex = _FIND_BIG_WORD_RE if WORD else _FIND_WORD_RE - iterable = regex.finditer(text) - - try: - for i, match in enumerate(iterable): - if i + 1 == count: - value = match.end(1) - - if include_current_position: - return value - else: - return value + 1 - - except StopIteration: - pass - return None - - def find_previous_word_beginning( - self, count: int = 1, WORD: bool = False - ) -> Optional[int]: - """ - Return an index relative to the cursor position pointing to the start - of the previous word. Return `None` if nothing was found. - """ - if count < 0: - return self.find_next_word_beginning(count=-count, WORD=WORD) - - regex = _FIND_BIG_WORD_RE if WORD else _FIND_WORD_RE - iterator = regex.finditer(self.text_before_cursor[::-1]) - - try: - for i, match in enumerate(iterator): - if i + 1 == count: - return -match.end(1) - except StopIteration: - pass - return None - - def find_previous_word_ending( - self, count: int = 1, WORD: bool = False - ) -> Optional[int]: - """ - Return an index relative to the cursor position pointing to the end - of the previous word. Return `None` if nothing was found. - """ - if count < 0: - return self.find_next_word_ending(count=-count, WORD=WORD) - - text_before_cursor = self.text_after_cursor[:1] + self.text_before_cursor[::-1] - - regex = _FIND_BIG_WORD_RE if WORD else _FIND_WORD_RE - iterator = regex.finditer(text_before_cursor) - - try: - for i, match in enumerate(iterator): - # Take first match, unless it's the word on which we're right now. - if i == 0 and match.start(1) == 0: - count += 1 - - if i + 1 == count: - return -match.start(1) + 1 - except StopIteration: - pass - return None - - def find_next_matching_line( - self, match_func: Callable[[str], bool], count: int = 1 - ) -> Optional[int]: - """ - Look downwards for empty lines. - Return the line index, relative to the current line. - """ - result = None - - for index, line in enumerate(self.lines[self.cursor_position_row + 1 :]): - if match_func(line): - result = 1 + index - count -= 1 - - if count == 0: - break - - return result - - def find_previous_matching_line( - self, match_func: Callable[[str], bool], count: int = 1 - ) -> Optional[int]: - """ - Look upwards for empty lines. - Return the line index, relative to the current line. - """ - result = None - - for index, line in enumerate(self.lines[: self.cursor_position_row][::-1]): - if match_func(line): - result = -1 - index - count -= 1 - - if count == 0: - break - - return result - - def get_cursor_left_position(self, count: int = 1) -> int: - """ - Relative position for cursor left. - """ - if count < 0: - return self.get_cursor_right_position(-count) - - return -min(self.cursor_position_col, count) - - def get_cursor_right_position(self, count: int = 1) -> int: - """ - Relative position for cursor_right. - """ - if count < 0: - return self.get_cursor_left_position(-count) - - return min(count, len(self.current_line_after_cursor)) - - def get_cursor_up_position( - self, count: int = 1, preferred_column: Optional[int] = None - ) -> int: - """ - Return the relative cursor position (character index) where we would be if the - user pressed the arrow-up button. - - :param preferred_column: When given, go to this column instead of - staying at the current column. - """ - assert count >= 1 - column = ( - self.cursor_position_col if preferred_column is None else preferred_column - ) - - return ( - self.translate_row_col_to_index( - max(0, self.cursor_position_row - count), column - ) - - self.cursor_position - ) - - def get_cursor_down_position( - self, count: int = 1, preferred_column: Optional[int] = None - ) -> int: - """ - Return the relative cursor position (character index) where we would be if the - user pressed the arrow-down button. - - :param preferred_column: When given, go to this column instead of - staying at the current column. - """ - assert count >= 1 - column = ( - self.cursor_position_col if preferred_column is None else preferred_column - ) - - return ( - self.translate_row_col_to_index(self.cursor_position_row + count, column) - - self.cursor_position - ) - - def find_enclosing_bracket_right( - self, left_ch: str, right_ch: str, end_pos: Optional[int] = None - ) -> Optional[int]: - """ - Find the right bracket enclosing current position. Return the relative - position to the cursor position. - - When `end_pos` is given, don't look past the position. - """ - if self.current_char == right_ch: - return 0 - - if end_pos is None: - end_pos = len(self.text) - else: - end_pos = min(len(self.text), end_pos) - - stack = 1 - - # Look forward. - for i in range(self.cursor_position + 1, end_pos): - c = self.text[i] - - if c == left_ch: - stack += 1 - elif c == right_ch: - stack -= 1 - - if stack == 0: - return i - self.cursor_position - - return None - - def find_enclosing_bracket_left( - self, left_ch: str, right_ch: str, start_pos: Optional[int] = None - ) -> Optional[int]: - """ - Find the left bracket enclosing current position. Return the relative - position to the cursor position. - - When `start_pos` is given, don't look past the position. - """ - if self.current_char == left_ch: - return 0 - - if start_pos is None: - start_pos = 0 - else: - start_pos = max(0, start_pos) - - stack = 1 - - # Look backward. - for i in range(self.cursor_position - 1, start_pos - 1, -1): - c = self.text[i] - - if c == right_ch: - stack += 1 - elif c == left_ch: - stack -= 1 - - if stack == 0: - return i - self.cursor_position - - return None - - def find_matching_bracket_position( - self, start_pos: Optional[int] = None, end_pos: Optional[int] = None - ) -> int: - """ - Return relative cursor position of matching [, (, { or < bracket. - - When `start_pos` or `end_pos` are given. Don't look past the positions. - """ - - # Look for a match. - for pair in "()", "[]", "{}", "<>": - A = pair[0] - B = pair[1] - if self.current_char == A: - return self.find_enclosing_bracket_right(A, B, end_pos=end_pos) or 0 - elif self.current_char == B: - return self.find_enclosing_bracket_left(A, B, start_pos=start_pos) or 0 - - return 0 - - def get_start_of_document_position(self) -> int: - """Relative position for the start of the document.""" - return -self.cursor_position - - def get_end_of_document_position(self) -> int: - """Relative position for the end of the document.""" - return len(self.text) - self.cursor_position - - def get_start_of_line_position(self, after_whitespace: bool = False) -> int: - """Relative position for the start of this line.""" - if after_whitespace: - current_line = self.current_line - return ( - len(current_line) - - len(current_line.lstrip()) - - self.cursor_position_col - ) - else: - return -len(self.current_line_before_cursor) - - def get_end_of_line_position(self) -> int: - """Relative position for the end of this line.""" - return len(self.current_line_after_cursor) - - def last_non_blank_of_current_line_position(self) -> int: - """ - Relative position for the last non blank character of this line. - """ - return len(self.current_line.rstrip()) - self.cursor_position_col - 1 - - def get_column_cursor_position(self, column: int) -> int: - """ - Return the relative cursor position for this column at the current - line. (It will stay between the boundaries of the line in case of a - larger number.) - """ - line_length = len(self.current_line) - current_column = self.cursor_position_col - column = max(0, min(line_length, column)) - - return column - current_column - - def selection_range( - self, - ) -> Tuple[ - int, int - ]: # XXX: shouldn't this return `None` if there is no selection??? - """ - Return (from, to) tuple of the selection. - start and end position are included. - - This doesn't take the selection type into account. Use - `selection_ranges` instead. - """ - if self.selection: - from_, to = sorted( - [self.cursor_position, self.selection.original_cursor_position] - ) - else: - from_, to = self.cursor_position, self.cursor_position - - return from_, to - - def selection_ranges(self) -> Iterable[Tuple[int, int]]: - """ - Return a list of `(from, to)` tuples for the selection or none if - nothing was selected. The upper boundary is not included. - - This will yield several (from, to) tuples in case of a BLOCK selection. - This will return zero ranges, like (8,8) for empty lines in a block - selection. - """ - if self.selection: - from_, to = sorted( - [self.cursor_position, self.selection.original_cursor_position] - ) - - if self.selection.type == SelectionType.BLOCK: - from_line, from_column = self.translate_index_to_position(from_) - to_line, to_column = self.translate_index_to_position(to) - from_column, to_column = sorted([from_column, to_column]) - lines = self.lines - - if vi_mode(): - to_column += 1 - - for l in range(from_line, to_line + 1): - line_length = len(lines[l]) - - if from_column <= line_length: - yield ( - self.translate_row_col_to_index(l, from_column), - self.translate_row_col_to_index( - l, min(line_length, to_column) - ), - ) - else: - # In case of a LINES selection, go to the start/end of the lines. - if self.selection.type == SelectionType.LINES: - from_ = max(0, self.text.rfind("\n", 0, from_) + 1) - - if self.text.find("\n", to) >= 0: - to = self.text.find("\n", to) - else: - to = len(self.text) - 1 - - # In Vi mode, the upper boundary is always included. For Emacs, - # that's not the case. - if vi_mode(): - to += 1 - - yield from_, to - - def selection_range_at_line(self, row: int) -> Optional[Tuple[int, int]]: - """ - If the selection spans a portion of the given line, return a (from, to) tuple. - - The returned upper boundary is not included in the selection, so - `(0, 0)` is an empty selection. `(0, 1)`, is a one character selection. - - Returns None if the selection doesn't cover this line at all. - """ - if self.selection: - line = self.lines[row] - - row_start = self.translate_row_col_to_index(row, 0) - row_end = self.translate_row_col_to_index(row, len(line)) - - from_, to = sorted( - [self.cursor_position, self.selection.original_cursor_position] - ) - - # Take the intersection of the current line and the selection. - intersection_start = max(row_start, from_) - intersection_end = min(row_end, to) - - if intersection_start <= intersection_end: - if self.selection.type == SelectionType.LINES: - intersection_start = row_start - intersection_end = row_end - - elif self.selection.type == SelectionType.BLOCK: - _, col1 = self.translate_index_to_position(from_) - _, col2 = self.translate_index_to_position(to) - col1, col2 = sorted([col1, col2]) - - if col1 > len(line): - return None # Block selection doesn't cross this line. - - intersection_start = self.translate_row_col_to_index(row, col1) - intersection_end = self.translate_row_col_to_index(row, col2) - - _, from_column = self.translate_index_to_position(intersection_start) - _, to_column = self.translate_index_to_position(intersection_end) - - # In Vi mode, the upper boundary is always included. For Emacs - # mode, that's not the case. - if vi_mode(): - to_column += 1 - - return from_column, to_column - return None - - def cut_selection(self) -> Tuple["Document", ClipboardData]: - """ - Return a (:class:`.Document`, :class:`.ClipboardData`) tuple, where the - document represents the new document when the selection is cut, and the - clipboard data, represents whatever has to be put on the clipboard. - """ - if self.selection: - cut_parts = [] - remaining_parts = [] - new_cursor_position = self.cursor_position - - last_to = 0 - for from_, to in self.selection_ranges(): - if last_to == 0: - new_cursor_position = from_ - - remaining_parts.append(self.text[last_to:from_]) - cut_parts.append(self.text[from_:to]) - last_to = to - - remaining_parts.append(self.text[last_to:]) - - cut_text = "\n".join(cut_parts) - remaining_text = "".join(remaining_parts) - - # In case of a LINES selection, don't include the trailing newline. - if self.selection.type == SelectionType.LINES and cut_text.endswith("\n"): - cut_text = cut_text[:-1] - - return ( - Document(text=remaining_text, cursor_position=new_cursor_position), - ClipboardData(cut_text, self.selection.type), - ) - else: - return self, ClipboardData("") - - def paste_clipboard_data( - self, - data: ClipboardData, - paste_mode: PasteMode = PasteMode.EMACS, - count: int = 1, - ) -> "Document": - """ - Return a new :class:`.Document` instance which contains the result if - we would paste this data at the current cursor position. - - :param paste_mode: Where to paste. (Before/after/emacs.) - :param count: When >1, Paste multiple times. - """ - before = paste_mode == PasteMode.VI_BEFORE - after = paste_mode == PasteMode.VI_AFTER - - if data.type == SelectionType.CHARACTERS: - if after: - new_text = ( - self.text[: self.cursor_position + 1] - + data.text * count - + self.text[self.cursor_position + 1 :] - ) - else: - new_text = ( - self.text_before_cursor + data.text * count + self.text_after_cursor - ) - - new_cursor_position = self.cursor_position + len(data.text) * count - if before: - new_cursor_position -= 1 - - elif data.type == SelectionType.LINES: - l = self.cursor_position_row - if before: - lines = self.lines[:l] + [data.text] * count + self.lines[l:] - new_text = "\n".join(lines) - new_cursor_position = len("".join(self.lines[:l])) + l - else: - lines = self.lines[: l + 1] + [data.text] * count + self.lines[l + 1 :] - new_cursor_position = len("".join(self.lines[: l + 1])) + l + 1 - new_text = "\n".join(lines) - - elif data.type == SelectionType.BLOCK: - lines = self.lines[:] - start_line = self.cursor_position_row - start_column = self.cursor_position_col + (0 if before else 1) - - for i, line in enumerate(data.text.split("\n")): - index = i + start_line - if index >= len(lines): - lines.append("") - - lines[index] = lines[index].ljust(start_column) - lines[index] = ( - lines[index][:start_column] - + line * count - + lines[index][start_column:] - ) - - new_text = "\n".join(lines) - new_cursor_position = self.cursor_position + (0 if before else 1) - - return Document(text=new_text, cursor_position=new_cursor_position) - - def empty_line_count_at_the_end(self) -> int: - """ - Return number of empty lines at the end of the document. - """ - count = 0 - for line in self.lines[::-1]: - if not line or line.isspace(): - count += 1 - else: - break - - return count - - def start_of_paragraph(self, count: int = 1, before: bool = False) -> int: - """ - Return the start of the current paragraph. (Relative cursor position.) - """ - - def match_func(text: str) -> bool: - return not text or text.isspace() - - line_index = self.find_previous_matching_line( - match_func=match_func, count=count - ) - - if line_index: - add = 0 if before else 1 - return min(0, self.get_cursor_up_position(count=-line_index) + add) - else: - return -self.cursor_position - - def end_of_paragraph(self, count: int = 1, after: bool = False) -> int: - """ - Return the end of the current paragraph. (Relative cursor position.) - """ - - def match_func(text: str) -> bool: - return not text or text.isspace() - - line_index = self.find_next_matching_line(match_func=match_func, count=count) - - if line_index: - add = 0 if after else 1 - return max(0, self.get_cursor_down_position(count=line_index) - add) - else: - return len(self.text_after_cursor) - - # Modifiers. - - def insert_after(self, text: str) -> "Document": - """ - Create a new document, with this text inserted after the buffer. - It keeps selection ranges and cursor position in sync. - """ - return Document( - text=self.text + text, - cursor_position=self.cursor_position, - selection=self.selection, - ) - - def insert_before(self, text: str) -> "Document": - """ - Create a new document, with this text inserted before the buffer. - It keeps selection ranges and cursor position in sync. - """ - selection_state = self.selection - - if selection_state: - selection_state = SelectionState( - original_cursor_position=selection_state.original_cursor_position - + len(text), - type=selection_state.type, - ) - - return Document( - text=text + self.text, - cursor_position=self.cursor_position + len(text), - selection=selection_state, - ) +""" +The `Document` that implements all the text operations/querying. +""" +import bisect +import re +import string +import weakref +from typing import ( + Callable, + Dict, + Iterable, + List, + NoReturn, + Optional, + Pattern, + Tuple, + cast, +) + +from .clipboard import ClipboardData +from .filters import vi_mode +from .selection import PasteMode, SelectionState, SelectionType + +__all__ = [ + "Document", +] + + +# Regex for finding "words" in documents. (We consider a group of alnum +# characters a word, but also a group of special characters a word, as long as +# it doesn't contain a space.) +# (This is a 'word' in Vi.) +_FIND_WORD_RE = re.compile(r"([a-zA-Z0-9_]+|[^a-zA-Z0-9_\s]+)") +_FIND_CURRENT_WORD_RE = re.compile(r"^([a-zA-Z0-9_]+|[^a-zA-Z0-9_\s]+)") +_FIND_CURRENT_WORD_INCLUDE_TRAILING_WHITESPACE_RE = re.compile( + r"^(([a-zA-Z0-9_]+|[^a-zA-Z0-9_\s]+)\s*)" +) + +# Regex for finding "WORDS" in documents. +# (This is a 'WORD in Vi.) +_FIND_BIG_WORD_RE = re.compile(r"([^\s]+)") +_FIND_CURRENT_BIG_WORD_RE = re.compile(r"^([^\s]+)") +_FIND_CURRENT_BIG_WORD_INCLUDE_TRAILING_WHITESPACE_RE = re.compile(r"^([^\s]+\s*)") + +# Share the Document._cache between all Document instances. +# (Document instances are considered immutable. That means that if another +# `Document` is constructed with the same text, it should have the same +# `_DocumentCache`.) +_text_to_document_cache: Dict[str, "_DocumentCache"] = cast( + Dict[str, "_DocumentCache"], + weakref.WeakValueDictionary(), # Maps document.text to DocumentCache instance. +) + + +class _ImmutableLineList(List[str]): + """ + Some protection for our 'lines' list, which is assumed to be immutable in the cache. + (Useful for detecting obvious bugs.) + """ + + def _error(self, *a: object, **kw: object) -> NoReturn: + raise NotImplementedError("Attempt to modify an immutable list.") + + __setitem__ = _error # type: ignore + append = _error + clear = _error + extend = _error + insert = _error + pop = _error + remove = _error + reverse = _error + sort = _error # type: ignore + + +class _DocumentCache: + def __init__(self) -> None: + #: List of lines for the Document text. + self.lines: Optional[_ImmutableLineList] = None + + #: List of index positions, pointing to the start of all the lines. + self.line_indexes: Optional[List[int]] = None + + +class Document: + """ + This is a immutable class around the text and cursor position, and contains + methods for querying this data, e.g. to give the text before the cursor. + + This class is usually instantiated by a :class:`~prompt_toolkit.buffer.Buffer` + object, and accessed as the `document` property of that class. + + :param text: string + :param cursor_position: int + :param selection: :class:`.SelectionState` + """ + + __slots__ = ("_text", "_cursor_position", "_selection", "_cache") + + def __init__( + self, + text: str = "", + cursor_position: Optional[int] = None, + selection: Optional[SelectionState] = None, + ) -> None: + + # Check cursor position. It can also be right after the end. (Where we + # insert text.) + assert cursor_position is None or cursor_position <= len(text), AssertionError( + "cursor_position=%r, len_text=%r" % (cursor_position, len(text)) + ) + + # By default, if no cursor position was given, make sure to put the + # cursor position is at the end of the document. This is what makes + # sense in most places. + if cursor_position is None: + cursor_position = len(text) + + # Keep these attributes private. A `Document` really has to be + # considered to be immutable, because otherwise the caching will break + # things. Because of that, we wrap these into read-only properties. + self._text = text + self._cursor_position = cursor_position + self._selection = selection + + # Cache for lines/indexes. (Shared with other Document instances that + # contain the same text. + try: + self._cache = _text_to_document_cache[self.text] + except KeyError: + self._cache = _DocumentCache() + _text_to_document_cache[self.text] = self._cache + + # XX: For some reason, above, we can't use 'WeakValueDictionary.setdefault'. + # This fails in Pypy3. `self._cache` becomes None, because that's what + # 'setdefault' returns. + # self._cache = _text_to_document_cache.setdefault(self.text, _DocumentCache()) + # assert self._cache + + def __repr__(self) -> str: + return "%s(%r, %r)" % (self.__class__.__name__, self.text, self.cursor_position) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Document): + return False + + return ( + self.text == other.text + and self.cursor_position == other.cursor_position + and self.selection == other.selection + ) + + @property + def text(self) -> str: + "The document text." + return self._text + + @property + def cursor_position(self) -> int: + "The document cursor position." + return self._cursor_position + + @property + def selection(self) -> Optional[SelectionState]: + ":class:`.SelectionState` object." + return self._selection + + @property + def current_char(self) -> str: + """Return character under cursor or an empty string.""" + return self._get_char_relative_to_cursor(0) or "" + + @property + def char_before_cursor(self) -> str: + """Return character before the cursor or an empty string.""" + return self._get_char_relative_to_cursor(-1) or "" + + @property + def text_before_cursor(self) -> str: + return self.text[: self.cursor_position :] + + @property + def text_after_cursor(self) -> str: + return self.text[self.cursor_position :] + + @property + def current_line_before_cursor(self) -> str: + """Text from the start of the line until the cursor.""" + _, _, text = self.text_before_cursor.rpartition("\n") + return text + + @property + def current_line_after_cursor(self) -> str: + """Text from the cursor until the end of the line.""" + text, _, _ = self.text_after_cursor.partition("\n") + return text + + @property + def lines(self) -> List[str]: + """ + Array of all the lines. + """ + # Cache, because this one is reused very often. + if self._cache.lines is None: + self._cache.lines = _ImmutableLineList(self.text.split("\n")) + + return self._cache.lines + + @property + def _line_start_indexes(self) -> List[int]: + """ + Array pointing to the start indexes of all the lines. + """ + # Cache, because this is often reused. (If it is used, it's often used + # many times. And this has to be fast for editing big documents!) + if self._cache.line_indexes is None: + # Create list of line lengths. + line_lengths = map(len, self.lines) + + # Calculate cumulative sums. + indexes = [0] + append = indexes.append + pos = 0 + + for line_length in line_lengths: + pos += line_length + 1 + append(pos) + + # Remove the last item. (This is not a new line.) + if len(indexes) > 1: + indexes.pop() + + self._cache.line_indexes = indexes + + return self._cache.line_indexes + + @property + def lines_from_current(self) -> List[str]: + """ + Array of the lines starting from the current line, until the last line. + """ + return self.lines[self.cursor_position_row :] + + @property + def line_count(self) -> int: + r"""Return the number of lines in this document. If the document ends + with a trailing \n, that counts as the beginning of a new line.""" + return len(self.lines) + + @property + def current_line(self) -> str: + """Return the text on the line where the cursor is. (when the input + consists of just one line, it equals `text`.""" + return self.current_line_before_cursor + self.current_line_after_cursor + + @property + def leading_whitespace_in_current_line(self) -> str: + """The leading whitespace in the left margin of the current line.""" + current_line = self.current_line + length = len(current_line) - len(current_line.lstrip()) + return current_line[:length] + + def _get_char_relative_to_cursor(self, offset: int = 0) -> str: + """ + Return character relative to cursor position, or empty string + """ + try: + return self.text[self.cursor_position + offset] + except IndexError: + return "" + + @property + def on_first_line(self) -> bool: + """ + True when we are at the first line. + """ + return self.cursor_position_row == 0 + + @property + def on_last_line(self) -> bool: + """ + True when we are at the last line. + """ + return self.cursor_position_row == self.line_count - 1 + + @property + def cursor_position_row(self) -> int: + """ + Current row. (0-based.) + """ + row, _ = self._find_line_start_index(self.cursor_position) + return row + + @property + def cursor_position_col(self) -> int: + """ + Current column. (0-based.) + """ + # (Don't use self.text_before_cursor to calculate this. Creating + # substrings and doing rsplit is too expensive for getting the cursor + # position.) + _, line_start_index = self._find_line_start_index(self.cursor_position) + return self.cursor_position - line_start_index + + def _find_line_start_index(self, index: int) -> Tuple[int, int]: + """ + For the index of a character at a certain line, calculate the index of + the first character on that line. + + Return (row, index) tuple. + """ + indexes = self._line_start_indexes + + pos = bisect.bisect_right(indexes, index) - 1 + return pos, indexes[pos] + + def translate_index_to_position(self, index: int) -> Tuple[int, int]: + """ + Given an index for the text, return the corresponding (row, col) tuple. + (0-based. Returns (0, 0) for index=0.) + """ + # Find start of this line. + row, row_index = self._find_line_start_index(index) + col = index - row_index + + return row, col + + def translate_row_col_to_index(self, row: int, col: int) -> int: + """ + Given a (row, col) tuple, return the corresponding index. + (Row and col params are 0-based.) + + Negative row/col values are turned into zero. + """ + try: + result = self._line_start_indexes[row] + line = self.lines[row] + except IndexError: + if row < 0: + result = self._line_start_indexes[0] + line = self.lines[0] + else: + result = self._line_start_indexes[-1] + line = self.lines[-1] + + result += max(0, min(col, len(line))) + + # Keep in range. (len(self.text) is included, because the cursor can be + # right after the end of the text as well.) + result = max(0, min(result, len(self.text))) + return result + + @property + def is_cursor_at_the_end(self) -> bool: + """True when the cursor is at the end of the text.""" + return self.cursor_position == len(self.text) + + @property + def is_cursor_at_the_end_of_line(self) -> bool: + """True when the cursor is at the end of this line.""" + return self.current_char in ("\n", "") + + def has_match_at_current_position(self, sub: str) -> bool: + """ + `True` when this substring is found at the cursor position. + """ + return self.text.find(sub, self.cursor_position) == self.cursor_position + + def find( + self, + sub: str, + in_current_line: bool = False, + include_current_position: bool = False, + ignore_case: bool = False, + count: int = 1, + ) -> Optional[int]: + """ + Find `text` after the cursor, return position relative to the cursor + position. Return `None` if nothing was found. + + :param count: Find the n-th occurrence. + """ + assert isinstance(ignore_case, bool) + + if in_current_line: + text = self.current_line_after_cursor + else: + text = self.text_after_cursor + + if not include_current_position: + if len(text) == 0: + return None # (Otherwise, we always get a match for the empty string.) + else: + text = text[1:] + + flags = re.IGNORECASE if ignore_case else 0 + iterator = re.finditer(re.escape(sub), text, flags) + + try: + for i, match in enumerate(iterator): + if i + 1 == count: + if include_current_position: + return match.start(0) + else: + return match.start(0) + 1 + except StopIteration: + pass + return None + + def find_all(self, sub: str, ignore_case: bool = False) -> List[int]: + """ + Find all occurrences of the substring. Return a list of absolute + positions in the document. + """ + flags = re.IGNORECASE if ignore_case else 0 + return [a.start() for a in re.finditer(re.escape(sub), self.text, flags)] + + def find_backwards( + self, + sub: str, + in_current_line: bool = False, + ignore_case: bool = False, + count: int = 1, + ) -> Optional[int]: + """ + Find `text` before the cursor, return position relative to the cursor + position. Return `None` if nothing was found. + + :param count: Find the n-th occurrence. + """ + if in_current_line: + before_cursor = self.current_line_before_cursor[::-1] + else: + before_cursor = self.text_before_cursor[::-1] + + flags = re.IGNORECASE if ignore_case else 0 + iterator = re.finditer(re.escape(sub[::-1]), before_cursor, flags) + + try: + for i, match in enumerate(iterator): + if i + 1 == count: + return -match.start(0) - len(sub) + except StopIteration: + pass + return None + + def get_word_before_cursor( + self, WORD: bool = False, pattern: Optional[Pattern[str]] = None + ) -> str: + """ + Give the word before the cursor. + If we have whitespace before the cursor this returns an empty string. + + :param pattern: (None or compiled regex). When given, use this regex + pattern. + """ + if self._is_word_before_cursor_complete(WORD=WORD, pattern=pattern): + # Space before the cursor or no text before cursor. + return "" + + text_before_cursor = self.text_before_cursor + start = self.find_start_of_previous_word(WORD=WORD, pattern=pattern) or 0 + + return text_before_cursor[len(text_before_cursor) + start :] + + def _is_word_before_cursor_complete( + self, WORD: bool = False, pattern: Optional[Pattern[str]] = None + ) -> bool: + if pattern: + return self.find_start_of_previous_word(WORD=WORD, pattern=pattern) is None + else: + return ( + self.text_before_cursor == "" or self.text_before_cursor[-1:].isspace() + ) + + def find_start_of_previous_word( + self, count: int = 1, WORD: bool = False, pattern: Optional[Pattern[str]] = None + ) -> Optional[int]: + """ + Return an index relative to the cursor position pointing to the start + of the previous word. Return `None` if nothing was found. + + :param pattern: (None or compiled regex). When given, use this regex + pattern. + """ + assert not (WORD and pattern) + + # Reverse the text before the cursor, in order to do an efficient + # backwards search. + text_before_cursor = self.text_before_cursor[::-1] + + if pattern: + regex = pattern + elif WORD: + regex = _FIND_BIG_WORD_RE + else: + regex = _FIND_WORD_RE + + iterator = regex.finditer(text_before_cursor) + + try: + for i, match in enumerate(iterator): + if i + 1 == count: + return -match.end(0) + except StopIteration: + pass + return None + + def find_boundaries_of_current_word( + self, + WORD: bool = False, + include_leading_whitespace: bool = False, + include_trailing_whitespace: bool = False, + ) -> Tuple[int, int]: + """ + Return the relative boundaries (startpos, endpos) of the current word under the + cursor. (This is at the current line, because line boundaries obviously + don't belong to any word.) + If not on a word, this returns (0,0) + """ + text_before_cursor = self.current_line_before_cursor[::-1] + text_after_cursor = self.current_line_after_cursor + + def get_regex(include_whitespace: bool) -> Pattern[str]: + return { + (False, False): _FIND_CURRENT_WORD_RE, + (False, True): _FIND_CURRENT_WORD_INCLUDE_TRAILING_WHITESPACE_RE, + (True, False): _FIND_CURRENT_BIG_WORD_RE, + (True, True): _FIND_CURRENT_BIG_WORD_INCLUDE_TRAILING_WHITESPACE_RE, + }[(WORD, include_whitespace)] + + match_before = get_regex(include_leading_whitespace).search(text_before_cursor) + match_after = get_regex(include_trailing_whitespace).search(text_after_cursor) + + # When there is a match before and after, and we're not looking for + # WORDs, make sure that both the part before and after the cursor are + # either in the [a-zA-Z_] alphabet or not. Otherwise, drop the part + # before the cursor. + if not WORD and match_before and match_after: + c1 = self.text[self.cursor_position - 1] + c2 = self.text[self.cursor_position] + alphabet = string.ascii_letters + "0123456789_" + + if (c1 in alphabet) != (c2 in alphabet): + match_before = None + + return ( + -match_before.end(1) if match_before else 0, + match_after.end(1) if match_after else 0, + ) + + def get_word_under_cursor(self, WORD: bool = False) -> str: + """ + Return the word, currently below the cursor. + This returns an empty string when the cursor is on a whitespace region. + """ + start, end = self.find_boundaries_of_current_word(WORD=WORD) + return self.text[self.cursor_position + start : self.cursor_position + end] + + def find_next_word_beginning( + self, count: int = 1, WORD: bool = False + ) -> Optional[int]: + """ + Return an index relative to the cursor position pointing to the start + of the next word. Return `None` if nothing was found. + """ + if count < 0: + return self.find_previous_word_beginning(count=-count, WORD=WORD) + + regex = _FIND_BIG_WORD_RE if WORD else _FIND_WORD_RE + iterator = regex.finditer(self.text_after_cursor) + + try: + for i, match in enumerate(iterator): + # Take first match, unless it's the word on which we're right now. + if i == 0 and match.start(1) == 0: + count += 1 + + if i + 1 == count: + return match.start(1) + except StopIteration: + pass + return None + + def find_next_word_ending( + self, include_current_position: bool = False, count: int = 1, WORD: bool = False + ) -> Optional[int]: + """ + Return an index relative to the cursor position pointing to the end + of the next word. Return `None` if nothing was found. + """ + if count < 0: + return self.find_previous_word_ending(count=-count, WORD=WORD) + + if include_current_position: + text = self.text_after_cursor + else: + text = self.text_after_cursor[1:] + + regex = _FIND_BIG_WORD_RE if WORD else _FIND_WORD_RE + iterable = regex.finditer(text) + + try: + for i, match in enumerate(iterable): + if i + 1 == count: + value = match.end(1) + + if include_current_position: + return value + else: + return value + 1 + + except StopIteration: + pass + return None + + def find_previous_word_beginning( + self, count: int = 1, WORD: bool = False + ) -> Optional[int]: + """ + Return an index relative to the cursor position pointing to the start + of the previous word. Return `None` if nothing was found. + """ + if count < 0: + return self.find_next_word_beginning(count=-count, WORD=WORD) + + regex = _FIND_BIG_WORD_RE if WORD else _FIND_WORD_RE + iterator = regex.finditer(self.text_before_cursor[::-1]) + + try: + for i, match in enumerate(iterator): + if i + 1 == count: + return -match.end(1) + except StopIteration: + pass + return None + + def find_previous_word_ending( + self, count: int = 1, WORD: bool = False + ) -> Optional[int]: + """ + Return an index relative to the cursor position pointing to the end + of the previous word. Return `None` if nothing was found. + """ + if count < 0: + return self.find_next_word_ending(count=-count, WORD=WORD) + + text_before_cursor = self.text_after_cursor[:1] + self.text_before_cursor[::-1] + + regex = _FIND_BIG_WORD_RE if WORD else _FIND_WORD_RE + iterator = regex.finditer(text_before_cursor) + + try: + for i, match in enumerate(iterator): + # Take first match, unless it's the word on which we're right now. + if i == 0 and match.start(1) == 0: + count += 1 + + if i + 1 == count: + return -match.start(1) + 1 + except StopIteration: + pass + return None + + def find_next_matching_line( + self, match_func: Callable[[str], bool], count: int = 1 + ) -> Optional[int]: + """ + Look downwards for empty lines. + Return the line index, relative to the current line. + """ + result = None + + for index, line in enumerate(self.lines[self.cursor_position_row + 1 :]): + if match_func(line): + result = 1 + index + count -= 1 + + if count == 0: + break + + return result + + def find_previous_matching_line( + self, match_func: Callable[[str], bool], count: int = 1 + ) -> Optional[int]: + """ + Look upwards for empty lines. + Return the line index, relative to the current line. + """ + result = None + + for index, line in enumerate(self.lines[: self.cursor_position_row][::-1]): + if match_func(line): + result = -1 - index + count -= 1 + + if count == 0: + break + + return result + + def get_cursor_left_position(self, count: int = 1) -> int: + """ + Relative position for cursor left. + """ + if count < 0: + return self.get_cursor_right_position(-count) + + return -min(self.cursor_position_col, count) + + def get_cursor_right_position(self, count: int = 1) -> int: + """ + Relative position for cursor_right. + """ + if count < 0: + return self.get_cursor_left_position(-count) + + return min(count, len(self.current_line_after_cursor)) + + def get_cursor_up_position( + self, count: int = 1, preferred_column: Optional[int] = None + ) -> int: + """ + Return the relative cursor position (character index) where we would be if the + user pressed the arrow-up button. + + :param preferred_column: When given, go to this column instead of + staying at the current column. + """ + assert count >= 1 + column = ( + self.cursor_position_col if preferred_column is None else preferred_column + ) + + return ( + self.translate_row_col_to_index( + max(0, self.cursor_position_row - count), column + ) + - self.cursor_position + ) + + def get_cursor_down_position( + self, count: int = 1, preferred_column: Optional[int] = None + ) -> int: + """ + Return the relative cursor position (character index) where we would be if the + user pressed the arrow-down button. + + :param preferred_column: When given, go to this column instead of + staying at the current column. + """ + assert count >= 1 + column = ( + self.cursor_position_col if preferred_column is None else preferred_column + ) + + return ( + self.translate_row_col_to_index(self.cursor_position_row + count, column) + - self.cursor_position + ) + + def find_enclosing_bracket_right( + self, left_ch: str, right_ch: str, end_pos: Optional[int] = None + ) -> Optional[int]: + """ + Find the right bracket enclosing current position. Return the relative + position to the cursor position. + + When `end_pos` is given, don't look past the position. + """ + if self.current_char == right_ch: + return 0 + + if end_pos is None: + end_pos = len(self.text) + else: + end_pos = min(len(self.text), end_pos) + + stack = 1 + + # Look forward. + for i in range(self.cursor_position + 1, end_pos): + c = self.text[i] + + if c == left_ch: + stack += 1 + elif c == right_ch: + stack -= 1 + + if stack == 0: + return i - self.cursor_position + + return None + + def find_enclosing_bracket_left( + self, left_ch: str, right_ch: str, start_pos: Optional[int] = None + ) -> Optional[int]: + """ + Find the left bracket enclosing current position. Return the relative + position to the cursor position. + + When `start_pos` is given, don't look past the position. + """ + if self.current_char == left_ch: + return 0 + + if start_pos is None: + start_pos = 0 + else: + start_pos = max(0, start_pos) + + stack = 1 + + # Look backward. + for i in range(self.cursor_position - 1, start_pos - 1, -1): + c = self.text[i] + + if c == right_ch: + stack += 1 + elif c == left_ch: + stack -= 1 + + if stack == 0: + return i - self.cursor_position + + return None + + def find_matching_bracket_position( + self, start_pos: Optional[int] = None, end_pos: Optional[int] = None + ) -> int: + """ + Return relative cursor position of matching [, (, { or < bracket. + + When `start_pos` or `end_pos` are given. Don't look past the positions. + """ + + # Look for a match. + for pair in "()", "[]", "{}", "<>": + A = pair[0] + B = pair[1] + if self.current_char == A: + return self.find_enclosing_bracket_right(A, B, end_pos=end_pos) or 0 + elif self.current_char == B: + return self.find_enclosing_bracket_left(A, B, start_pos=start_pos) or 0 + + return 0 + + def get_start_of_document_position(self) -> int: + """Relative position for the start of the document.""" + return -self.cursor_position + + def get_end_of_document_position(self) -> int: + """Relative position for the end of the document.""" + return len(self.text) - self.cursor_position + + def get_start_of_line_position(self, after_whitespace: bool = False) -> int: + """Relative position for the start of this line.""" + if after_whitespace: + current_line = self.current_line + return ( + len(current_line) + - len(current_line.lstrip()) + - self.cursor_position_col + ) + else: + return -len(self.current_line_before_cursor) + + def get_end_of_line_position(self) -> int: + """Relative position for the end of this line.""" + return len(self.current_line_after_cursor) + + def last_non_blank_of_current_line_position(self) -> int: + """ + Relative position for the last non blank character of this line. + """ + return len(self.current_line.rstrip()) - self.cursor_position_col - 1 + + def get_column_cursor_position(self, column: int) -> int: + """ + Return the relative cursor position for this column at the current + line. (It will stay between the boundaries of the line in case of a + larger number.) + """ + line_length = len(self.current_line) + current_column = self.cursor_position_col + column = max(0, min(line_length, column)) + + return column - current_column + + def selection_range( + self, + ) -> Tuple[ + int, int + ]: # XXX: shouldn't this return `None` if there is no selection??? + """ + Return (from, to) tuple of the selection. + start and end position are included. + + This doesn't take the selection type into account. Use + `selection_ranges` instead. + """ + if self.selection: + from_, to = sorted( + [self.cursor_position, self.selection.original_cursor_position] + ) + else: + from_, to = self.cursor_position, self.cursor_position + + return from_, to + + def selection_ranges(self) -> Iterable[Tuple[int, int]]: + """ + Return a list of `(from, to)` tuples for the selection or none if + nothing was selected. The upper boundary is not included. + + This will yield several (from, to) tuples in case of a BLOCK selection. + This will return zero ranges, like (8,8) for empty lines in a block + selection. + """ + if self.selection: + from_, to = sorted( + [self.cursor_position, self.selection.original_cursor_position] + ) + + if self.selection.type == SelectionType.BLOCK: + from_line, from_column = self.translate_index_to_position(from_) + to_line, to_column = self.translate_index_to_position(to) + from_column, to_column = sorted([from_column, to_column]) + lines = self.lines + + if vi_mode(): + to_column += 1 + + for l in range(from_line, to_line + 1): + line_length = len(lines[l]) + + if from_column <= line_length: + yield ( + self.translate_row_col_to_index(l, from_column), + self.translate_row_col_to_index( + l, min(line_length, to_column) + ), + ) + else: + # In case of a LINES selection, go to the start/end of the lines. + if self.selection.type == SelectionType.LINES: + from_ = max(0, self.text.rfind("\n", 0, from_) + 1) + + if self.text.find("\n", to) >= 0: + to = self.text.find("\n", to) + else: + to = len(self.text) - 1 + + # In Vi mode, the upper boundary is always included. For Emacs, + # that's not the case. + if vi_mode(): + to += 1 + + yield from_, to + + def selection_range_at_line(self, row: int) -> Optional[Tuple[int, int]]: + """ + If the selection spans a portion of the given line, return a (from, to) tuple. + + The returned upper boundary is not included in the selection, so + `(0, 0)` is an empty selection. `(0, 1)`, is a one character selection. + + Returns None if the selection doesn't cover this line at all. + """ + if self.selection: + line = self.lines[row] + + row_start = self.translate_row_col_to_index(row, 0) + row_end = self.translate_row_col_to_index(row, len(line)) + + from_, to = sorted( + [self.cursor_position, self.selection.original_cursor_position] + ) + + # Take the intersection of the current line and the selection. + intersection_start = max(row_start, from_) + intersection_end = min(row_end, to) + + if intersection_start <= intersection_end: + if self.selection.type == SelectionType.LINES: + intersection_start = row_start + intersection_end = row_end + + elif self.selection.type == SelectionType.BLOCK: + _, col1 = self.translate_index_to_position(from_) + _, col2 = self.translate_index_to_position(to) + col1, col2 = sorted([col1, col2]) + + if col1 > len(line): + return None # Block selection doesn't cross this line. + + intersection_start = self.translate_row_col_to_index(row, col1) + intersection_end = self.translate_row_col_to_index(row, col2) + + _, from_column = self.translate_index_to_position(intersection_start) + _, to_column = self.translate_index_to_position(intersection_end) + + # In Vi mode, the upper boundary is always included. For Emacs + # mode, that's not the case. + if vi_mode(): + to_column += 1 + + return from_column, to_column + return None + + def cut_selection(self) -> Tuple["Document", ClipboardData]: + """ + Return a (:class:`.Document`, :class:`.ClipboardData`) tuple, where the + document represents the new document when the selection is cut, and the + clipboard data, represents whatever has to be put on the clipboard. + """ + if self.selection: + cut_parts = [] + remaining_parts = [] + new_cursor_position = self.cursor_position + + last_to = 0 + for from_, to in self.selection_ranges(): + if last_to == 0: + new_cursor_position = from_ + + remaining_parts.append(self.text[last_to:from_]) + cut_parts.append(self.text[from_:to]) + last_to = to + + remaining_parts.append(self.text[last_to:]) + + cut_text = "\n".join(cut_parts) + remaining_text = "".join(remaining_parts) + + # In case of a LINES selection, don't include the trailing newline. + if self.selection.type == SelectionType.LINES and cut_text.endswith("\n"): + cut_text = cut_text[:-1] + + return ( + Document(text=remaining_text, cursor_position=new_cursor_position), + ClipboardData(cut_text, self.selection.type), + ) + else: + return self, ClipboardData("") + + def paste_clipboard_data( + self, + data: ClipboardData, + paste_mode: PasteMode = PasteMode.EMACS, + count: int = 1, + ) -> "Document": + """ + Return a new :class:`.Document` instance which contains the result if + we would paste this data at the current cursor position. + + :param paste_mode: Where to paste. (Before/after/emacs.) + :param count: When >1, Paste multiple times. + """ + before = paste_mode == PasteMode.VI_BEFORE + after = paste_mode == PasteMode.VI_AFTER + + if data.type == SelectionType.CHARACTERS: + if after: + new_text = ( + self.text[: self.cursor_position + 1] + + data.text * count + + self.text[self.cursor_position + 1 :] + ) + else: + new_text = ( + self.text_before_cursor + data.text * count + self.text_after_cursor + ) + + new_cursor_position = self.cursor_position + len(data.text) * count + if before: + new_cursor_position -= 1 + + elif data.type == SelectionType.LINES: + l = self.cursor_position_row + if before: + lines = self.lines[:l] + [data.text] * count + self.lines[l:] + new_text = "\n".join(lines) + new_cursor_position = len("".join(self.lines[:l])) + l + else: + lines = self.lines[: l + 1] + [data.text] * count + self.lines[l + 1 :] + new_cursor_position = len("".join(self.lines[: l + 1])) + l + 1 + new_text = "\n".join(lines) + + elif data.type == SelectionType.BLOCK: + lines = self.lines[:] + start_line = self.cursor_position_row + start_column = self.cursor_position_col + (0 if before else 1) + + for i, line in enumerate(data.text.split("\n")): + index = i + start_line + if index >= len(lines): + lines.append("") + + lines[index] = lines[index].ljust(start_column) + lines[index] = ( + lines[index][:start_column] + + line * count + + lines[index][start_column:] + ) + + new_text = "\n".join(lines) + new_cursor_position = self.cursor_position + (0 if before else 1) + + return Document(text=new_text, cursor_position=new_cursor_position) + + def empty_line_count_at_the_end(self) -> int: + """ + Return number of empty lines at the end of the document. + """ + count = 0 + for line in self.lines[::-1]: + if not line or line.isspace(): + count += 1 + else: + break + + return count + + def start_of_paragraph(self, count: int = 1, before: bool = False) -> int: + """ + Return the start of the current paragraph. (Relative cursor position.) + """ + + def match_func(text: str) -> bool: + return not text or text.isspace() + + line_index = self.find_previous_matching_line( + match_func=match_func, count=count + ) + + if line_index: + add = 0 if before else 1 + return min(0, self.get_cursor_up_position(count=-line_index) + add) + else: + return -self.cursor_position + + def end_of_paragraph(self, count: int = 1, after: bool = False) -> int: + """ + Return the end of the current paragraph. (Relative cursor position.) + """ + + def match_func(text: str) -> bool: + return not text or text.isspace() + + line_index = self.find_next_matching_line(match_func=match_func, count=count) + + if line_index: + add = 0 if after else 1 + return max(0, self.get_cursor_down_position(count=line_index) - add) + else: + return len(self.text_after_cursor) + + # Modifiers. + + def insert_after(self, text: str) -> "Document": + """ + Create a new document, with this text inserted after the buffer. + It keeps selection ranges and cursor position in sync. + """ + return Document( + text=self.text + text, + cursor_position=self.cursor_position, + selection=self.selection, + ) + + def insert_before(self, text: str) -> "Document": + """ + Create a new document, with this text inserted before the buffer. + It keeps selection ranges and cursor position in sync. + """ + selection_state = self.selection + + if selection_state: + selection_state = SelectionState( + original_cursor_position=selection_state.original_cursor_position + + len(text), + type=selection_state.type, + ) + + return Document( + text=text + self.text, + cursor_position=self.cursor_position + len(text), + selection=selection_state, + ) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/enums.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/enums.py index 64fc6eb628..4f496e67ad 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/enums.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/enums.py @@ -1,17 +1,17 @@ -from enum import Enum - - -class EditingMode(Enum): - # The set of key bindings that is active. - VI = "VI" - EMACS = "EMACS" - - -#: Name of the search buffer. -SEARCH_BUFFER = "SEARCH_BUFFER" - -#: Name of the default buffer. -DEFAULT_BUFFER = "DEFAULT_BUFFER" - -#: Name of the system buffer. -SYSTEM_BUFFER = "SYSTEM_BUFFER" +from enum import Enum + + +class EditingMode(Enum): + # The set of key bindings that is active. + VI = "VI" + EMACS = "EMACS" + + +#: Name of the search buffer. +SEARCH_BUFFER = "SEARCH_BUFFER" + +#: Name of the default buffer. +DEFAULT_BUFFER = "DEFAULT_BUFFER" + +#: Name of the system buffer. +SYSTEM_BUFFER = "SYSTEM_BUFFER" diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/__init__.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/__init__.py index e2f86d01e7..3a3e9acf22 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/__init__.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/__init__.py @@ -1,28 +1,28 @@ -from .async_generator import generator_to_async_generator -from .inputhook import ( - InputHookContext, - InputHookSelector, - new_eventloop_with_inputhook, - set_eventloop_with_inputhook, -) -from .utils import ( - call_soon_threadsafe, - get_event_loop, - get_traceback_from_context, - run_in_executor_with_context, -) - -__all__ = [ - # Async generator - "generator_to_async_generator", - # Utils. - "run_in_executor_with_context", - "call_soon_threadsafe", - "get_traceback_from_context", - "get_event_loop", - # Inputhooks. - "new_eventloop_with_inputhook", - "set_eventloop_with_inputhook", - "InputHookSelector", - "InputHookContext", -] +from .async_generator import generator_to_async_generator +from .inputhook import ( + InputHookContext, + InputHookSelector, + new_eventloop_with_inputhook, + set_eventloop_with_inputhook, +) +from .utils import ( + call_soon_threadsafe, + get_event_loop, + get_traceback_from_context, + run_in_executor_with_context, +) + +__all__ = [ + # Async generator + "generator_to_async_generator", + # Utils. + "run_in_executor_with_context", + "call_soon_threadsafe", + "get_traceback_from_context", + "get_event_loop", + # Inputhooks. + "new_eventloop_with_inputhook", + "set_eventloop_with_inputhook", + "InputHookSelector", + "InputHookContext", +] diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/async_context_manager.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/async_context_manager.py index 851942a2de..39146165a0 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/async_context_manager.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/async_context_manager.py @@ -1,132 +1,132 @@ -""" -@asynccontextmanager code, copied from Python 3.7's contextlib. -For usage in Python 3.6. -Types have been added to this file, just enough to make Mypy happy. -""" -# mypy: allow-untyped-defs -import abc -from functools import wraps +""" +@asynccontextmanager code, copied from Python 3.7's contextlib. +For usage in Python 3.6. +Types have been added to this file, just enough to make Mypy happy. +""" +# mypy: allow-untyped-defs +import abc +from functools import wraps from typing import AsyncContextManager, AsyncIterator, Callable, TypeVar - -import _collections_abc - -__all__ = ["asynccontextmanager"] - - -class AbstractAsyncContextManager(abc.ABC): - - """An abstract base class for asynchronous context managers.""" - - async def __aenter__(self): - """Return `self` upon entering the runtime context.""" - return self - - @abc.abstractmethod - async def __aexit__(self, exc_type, exc_value, traceback): - """Raise any exception triggered within the runtime context.""" - return None - - @classmethod - def __subclasshook__(cls, C): - if cls is AbstractAsyncContextManager: - return _collections_abc._check_methods(C, "__aenter__", "__aexit__") # type: ignore - return NotImplemented - - -class _GeneratorContextManagerBase: - """Shared functionality for @contextmanager and @asynccontextmanager.""" - - def __init__(self, func, args, kwds): - self.gen = func(*args, **kwds) - self.func, self.args, self.kwds = func, args, kwds - # Issue 19330: ensure context manager instances have good docstrings - doc = getattr(func, "__doc__", None) - if doc is None: - doc = type(self).__doc__ - self.__doc__ = doc - # Unfortunately, this still doesn't provide good help output when - # inspecting the created context manager instances, since pydoc - # currently bypasses the instance docstring and shows the docstring - # for the class instead. - # See http://bugs.python.org/issue19404 for more details. - - -class _AsyncGeneratorContextManager( - _GeneratorContextManagerBase, AbstractAsyncContextManager -): - """Helper for @asynccontextmanager.""" - - async def __aenter__(self): - try: - return await self.gen.__anext__() - except StopAsyncIteration: - raise RuntimeError("generator didn't yield") from None - - async def __aexit__(self, typ, value, traceback): - if typ is None: - try: - await self.gen.__anext__() - except StopAsyncIteration: - return - else: - raise RuntimeError("generator didn't stop") - else: - if value is None: - value = typ() - # See _GeneratorContextManager.__exit__ for comments on subtleties - # in this implementation - try: - await self.gen.athrow(typ, value, traceback) - raise RuntimeError("generator didn't stop after athrow()") - except StopAsyncIteration as exc: - return exc is not value - except RuntimeError as exc: - if exc is value: - return False - # Avoid suppressing if a StopIteration exception - # was passed to throw() and later wrapped into a RuntimeError - # (see PEP 479 for sync generators; async generators also - # have this behavior). But do this only if the exception wrapped - # by the RuntimeError is actully Stop(Async)Iteration (see - # issue29692). - if isinstance(value, (StopIteration, StopAsyncIteration)): - if exc.__cause__ is value: - return False - raise - except BaseException as exc: - if exc is not value: - raise - - -_T = TypeVar("_T") - - -def asynccontextmanager( - func: Callable[..., AsyncIterator[_T]] -) -> Callable[..., AsyncContextManager[_T]]: - """@asynccontextmanager decorator. - Typical usage: - @asynccontextmanager - async def some_async_generator(<arguments>): - <setup> - try: - yield <value> - finally: - <cleanup> - This makes this: - async with some_async_generator(<arguments>) as <variable>: - <body> - equivalent to this: - <setup> - try: - <variable> = <value> - <body> - finally: - <cleanup> - """ - - @wraps(func) - def helper(*args, **kwds): - return _AsyncGeneratorContextManager(func, args, kwds) # type: ignore - - return helper + +import _collections_abc + +__all__ = ["asynccontextmanager"] + + +class AbstractAsyncContextManager(abc.ABC): + + """An abstract base class for asynchronous context managers.""" + + async def __aenter__(self): + """Return `self` upon entering the runtime context.""" + return self + + @abc.abstractmethod + async def __aexit__(self, exc_type, exc_value, traceback): + """Raise any exception triggered within the runtime context.""" + return None + + @classmethod + def __subclasshook__(cls, C): + if cls is AbstractAsyncContextManager: + return _collections_abc._check_methods(C, "__aenter__", "__aexit__") # type: ignore + return NotImplemented + + +class _GeneratorContextManagerBase: + """Shared functionality for @contextmanager and @asynccontextmanager.""" + + def __init__(self, func, args, kwds): + self.gen = func(*args, **kwds) + self.func, self.args, self.kwds = func, args, kwds + # Issue 19330: ensure context manager instances have good docstrings + doc = getattr(func, "__doc__", None) + if doc is None: + doc = type(self).__doc__ + self.__doc__ = doc + # Unfortunately, this still doesn't provide good help output when + # inspecting the created context manager instances, since pydoc + # currently bypasses the instance docstring and shows the docstring + # for the class instead. + # See http://bugs.python.org/issue19404 for more details. + + +class _AsyncGeneratorContextManager( + _GeneratorContextManagerBase, AbstractAsyncContextManager +): + """Helper for @asynccontextmanager.""" + + async def __aenter__(self): + try: + return await self.gen.__anext__() + except StopAsyncIteration: + raise RuntimeError("generator didn't yield") from None + + async def __aexit__(self, typ, value, traceback): + if typ is None: + try: + await self.gen.__anext__() + except StopAsyncIteration: + return + else: + raise RuntimeError("generator didn't stop") + else: + if value is None: + value = typ() + # See _GeneratorContextManager.__exit__ for comments on subtleties + # in this implementation + try: + await self.gen.athrow(typ, value, traceback) + raise RuntimeError("generator didn't stop after athrow()") + except StopAsyncIteration as exc: + return exc is not value + except RuntimeError as exc: + if exc is value: + return False + # Avoid suppressing if a StopIteration exception + # was passed to throw() and later wrapped into a RuntimeError + # (see PEP 479 for sync generators; async generators also + # have this behavior). But do this only if the exception wrapped + # by the RuntimeError is actully Stop(Async)Iteration (see + # issue29692). + if isinstance(value, (StopIteration, StopAsyncIteration)): + if exc.__cause__ is value: + return False + raise + except BaseException as exc: + if exc is not value: + raise + + +_T = TypeVar("_T") + + +def asynccontextmanager( + func: Callable[..., AsyncIterator[_T]] +) -> Callable[..., AsyncContextManager[_T]]: + """@asynccontextmanager decorator. + Typical usage: + @asynccontextmanager + async def some_async_generator(<arguments>): + <setup> + try: + yield <value> + finally: + <cleanup> + This makes this: + async with some_async_generator(<arguments>) as <variable>: + <body> + equivalent to this: + <setup> + try: + <variable> = <value> + <body> + finally: + <cleanup> + """ + + @wraps(func) + def helper(*args, **kwds): + return _AsyncGeneratorContextManager(func, args, kwds) # type: ignore + + return helper diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/async_generator.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/async_generator.py index dd520d1bc1..7ab3c28673 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/async_generator.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/async_generator.py @@ -1,74 +1,74 @@ -""" -Implementation for async generators. -""" -from asyncio import Queue -from typing import AsyncGenerator, Callable, Iterable, TypeVar, Union - -from .utils import get_event_loop, run_in_executor_with_context - -__all__ = [ - "generator_to_async_generator", -] - - -_T = TypeVar("_T") - - -class _Done: - pass - - -async def generator_to_async_generator( - get_iterable: Callable[[], Iterable[_T]] -) -> AsyncGenerator[_T, None]: - """ - Turn a generator or iterable into an async generator. - - This works by running the generator in a background thread. - - :param get_iterable: Function that returns a generator or iterable when - called. - """ - quitting = False - _done = _Done() - q: Queue[Union[_T, _Done]] = Queue() - loop = get_event_loop() - - def runner() -> None: - """ - Consume the generator in background thread. - When items are received, they'll be pushed to the queue. - """ - try: - for item in get_iterable(): - # When this async generator was cancelled (closed), stop this - # thread. - if quitting: - break - - loop.call_soon_threadsafe(q.put_nowait, item) - - finally: - loop.call_soon_threadsafe(q.put_nowait, _done) - - # Start background thread. - runner_f = run_in_executor_with_context(runner) - - try: - while True: - item = await q.get() - if isinstance(item, _Done): - break - else: - yield item - finally: - # When this async generator is closed (GeneratorExit exception, stop - # the background thread as well. - we don't need that anymore.) - quitting = True - - # Wait for the background thread to finish. (should happen right after - # the next item is yielded). If we don't do this, and the event loop - # gets closed before the runner is done, then we'll get a - # `RuntimeError: Event loop is closed` exception printed to stdout that - # we can't handle. - await runner_f +""" +Implementation for async generators. +""" +from asyncio import Queue +from typing import AsyncGenerator, Callable, Iterable, TypeVar, Union + +from .utils import get_event_loop, run_in_executor_with_context + +__all__ = [ + "generator_to_async_generator", +] + + +_T = TypeVar("_T") + + +class _Done: + pass + + +async def generator_to_async_generator( + get_iterable: Callable[[], Iterable[_T]] +) -> AsyncGenerator[_T, None]: + """ + Turn a generator or iterable into an async generator. + + This works by running the generator in a background thread. + + :param get_iterable: Function that returns a generator or iterable when + called. + """ + quitting = False + _done = _Done() + q: Queue[Union[_T, _Done]] = Queue() + loop = get_event_loop() + + def runner() -> None: + """ + Consume the generator in background thread. + When items are received, they'll be pushed to the queue. + """ + try: + for item in get_iterable(): + # When this async generator was cancelled (closed), stop this + # thread. + if quitting: + break + + loop.call_soon_threadsafe(q.put_nowait, item) + + finally: + loop.call_soon_threadsafe(q.put_nowait, _done) + + # Start background thread. + runner_f = run_in_executor_with_context(runner) + + try: + while True: + item = await q.get() + if isinstance(item, _Done): + break + else: + yield item + finally: + # When this async generator is closed (GeneratorExit exception, stop + # the background thread as well. - we don't need that anymore.) + quitting = True + + # Wait for the background thread to finish. (should happen right after + # the next item is yielded). If we don't do this, and the event loop + # gets closed before the runner is done, then we'll get a + # `RuntimeError: Event loop is closed` exception printed to stdout that + # we can't handle. + await runner_f diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/dummy_contextvars.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/dummy_contextvars.py index 2c8deb935f..2b20d69b42 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/dummy_contextvars.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/dummy_contextvars.py @@ -1,49 +1,49 @@ -""" -Dummy contextvars implementation, to make prompt_toolkit work on Python 3.6. - -As long as there is only one application running at a time, we don't need the -real contextvars. So, stuff like the telnet-server and so on requires 3.7. -""" -from typing import Any, Callable, Generic, Optional, TypeVar - - -def copy_context() -> "Context": - return Context() - - -_T = TypeVar("_T") - - -class Context: - def run(self, callable: Callable[..., _T], *args: Any, **kwargs: Any) -> _T: - return callable(*args, **kwargs) - - def copy(self) -> "Context": - return self - - -class Token(Generic[_T]): - pass - - -class ContextVar(Generic[_T]): - def __init__(self, name: str, *, default: Optional[_T] = None) -> None: - self._name = name - self._value = default - - @property - def name(self) -> str: - return self._name - - def get(self, default: Optional[_T] = None) -> _T: - result = self._value or default - if result is None: - raise LookupError - return result - - def set(self, value: _T) -> Token[_T]: - self._value = value - return Token() - - def reset(self, token: Token[_T]) -> None: - pass +""" +Dummy contextvars implementation, to make prompt_toolkit work on Python 3.6. + +As long as there is only one application running at a time, we don't need the +real contextvars. So, stuff like the telnet-server and so on requires 3.7. +""" +from typing import Any, Callable, Generic, Optional, TypeVar + + +def copy_context() -> "Context": + return Context() + + +_T = TypeVar("_T") + + +class Context: + def run(self, callable: Callable[..., _T], *args: Any, **kwargs: Any) -> _T: + return callable(*args, **kwargs) + + def copy(self) -> "Context": + return self + + +class Token(Generic[_T]): + pass + + +class ContextVar(Generic[_T]): + def __init__(self, name: str, *, default: Optional[_T] = None) -> None: + self._name = name + self._value = default + + @property + def name(self) -> str: + return self._name + + def get(self, default: Optional[_T] = None) -> _T: + result = self._value or default + if result is None: + raise LookupError + return result + + def set(self, value: _T) -> Token[_T]: + self._value = value + return Token() + + def reset(self, token: Token[_T]) -> None: + pass diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/inputhook.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/inputhook.py index 54118f8355..26228a2af3 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/inputhook.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/inputhook.py @@ -1,184 +1,184 @@ -""" -Similar to `PyOS_InputHook` of the Python API, we can plug in an input hook in -the asyncio event loop. - -The way this works is by using a custom 'selector' that runs the other event -loop until the real selector is ready. - -It's the responsibility of this event hook to return when there is input ready. -There are two ways to detect when input is ready: - -The inputhook itself is a callable that receives an `InputHookContext`. This -callable should run the other event loop, and return when the main loop has -stuff to do. There are two ways to detect when to return: - -- Call the `input_is_ready` method periodically. Quit when this returns `True`. - -- Add the `fileno` as a watch to the external eventloop. Quit when file descriptor - becomes readable. (But don't read from it.) - - Note that this is not the same as checking for `sys.stdin.fileno()`. The - eventloop of prompt-toolkit allows thread-based executors, for example for - asynchronous autocompletion. When the completion for instance is ready, we - also want prompt-toolkit to gain control again in order to display that. -""" -import asyncio -import os -import select -import selectors -import threading -from asyncio import AbstractEventLoop -from selectors import BaseSelector, SelectorKey +""" +Similar to `PyOS_InputHook` of the Python API, we can plug in an input hook in +the asyncio event loop. + +The way this works is by using a custom 'selector' that runs the other event +loop until the real selector is ready. + +It's the responsibility of this event hook to return when there is input ready. +There are two ways to detect when input is ready: + +The inputhook itself is a callable that receives an `InputHookContext`. This +callable should run the other event loop, and return when the main loop has +stuff to do. There are two ways to detect when to return: + +- Call the `input_is_ready` method periodically. Quit when this returns `True`. + +- Add the `fileno` as a watch to the external eventloop. Quit when file descriptor + becomes readable. (But don't read from it.) + + Note that this is not the same as checking for `sys.stdin.fileno()`. The + eventloop of prompt-toolkit allows thread-based executors, for example for + asynchronous autocompletion. When the completion for instance is ready, we + also want prompt-toolkit to gain control again in order to display that. +""" +import asyncio +import os +import select +import selectors +import threading +from asyncio import AbstractEventLoop +from selectors import BaseSelector, SelectorKey from typing import TYPE_CHECKING, Any, Callable, List, Mapping, Optional, Tuple - -from prompt_toolkit.utils import is_windows - -from .utils import get_event_loop - -__all__ = [ - "new_eventloop_with_inputhook", - "set_eventloop_with_inputhook", - "InputHookSelector", - "InputHookContext", -] - -if TYPE_CHECKING: + +from prompt_toolkit.utils import is_windows + +from .utils import get_event_loop + +__all__ = [ + "new_eventloop_with_inputhook", + "set_eventloop_with_inputhook", + "InputHookSelector", + "InputHookContext", +] + +if TYPE_CHECKING: from _typeshed import FileDescriptorLike - - _EventMask = int - - -def new_eventloop_with_inputhook( - inputhook: Callable[["InputHookContext"], None] -) -> AbstractEventLoop: - """ - Create a new event loop with the given inputhook. - """ - selector = InputHookSelector(selectors.DefaultSelector(), inputhook) - loop = asyncio.SelectorEventLoop(selector) - return loop - - -def set_eventloop_with_inputhook( - inputhook: Callable[["InputHookContext"], None] -) -> AbstractEventLoop: - """ - Create a new event loop with the given inputhook, and activate it. - """ - loop = new_eventloop_with_inputhook(inputhook) - asyncio.set_event_loop(loop) - return loop - - -class InputHookSelector(BaseSelector): - """ - Usage: - - selector = selectors.SelectSelector() - loop = asyncio.SelectorEventLoop(InputHookSelector(selector, inputhook)) - asyncio.set_event_loop(loop) - """ - - def __init__( - self, selector: BaseSelector, inputhook: Callable[["InputHookContext"], None] - ) -> None: - self.selector = selector - self.inputhook = inputhook - self._r, self._w = os.pipe() - - def register( - self, fileobj: "FileDescriptorLike", events: "_EventMask", data: Any = None - ) -> "SelectorKey": - return self.selector.register(fileobj, events, data=data) - - def unregister(self, fileobj: "FileDescriptorLike") -> "SelectorKey": - return self.selector.unregister(fileobj) - - def modify( - self, fileobj: "FileDescriptorLike", events: "_EventMask", data: Any = None - ) -> "SelectorKey": - return self.selector.modify(fileobj, events, data=None) - - def select( - self, timeout: Optional[float] = None - ) -> List[Tuple["SelectorKey", "_EventMask"]]: - # If there are tasks in the current event loop, - # don't run the input hook. - if len(getattr(get_event_loop(), "_ready", [])) > 0: - return self.selector.select(timeout=timeout) - - ready = False - result = None - - # Run selector in other thread. - def run_selector() -> None: - nonlocal ready, result - result = self.selector.select(timeout=timeout) - os.write(self._w, b"x") - ready = True - - th = threading.Thread(target=run_selector) - th.start() - - def input_is_ready() -> bool: - return ready - - # Call inputhook. - # The inputhook function is supposed to return when our selector - # becomes ready. The inputhook can do that by registering the fd in its - # own loop, or by checking the `input_is_ready` function regularly. - self.inputhook(InputHookContext(self._r, input_is_ready)) - - # Flush the read end of the pipe. - try: - # Before calling 'os.read', call select.select. This is required - # when the gevent monkey patch has been applied. 'os.read' is never - # monkey patched and won't be cooperative, so that would block all - # other select() calls otherwise. - # See: http://www.gevent.org/gevent.os.html - - # Note: On Windows, this is apparently not an issue. - # However, if we would ever want to add a select call, it - # should use `windll.kernel32.WaitForMultipleObjects`, - # because `select.select` can't wait for a pipe on Windows. - if not is_windows(): - select.select([self._r], [], [], None) - - os.read(self._r, 1024) - except OSError: - # This happens when the window resizes and a SIGWINCH was received. - # We get 'Error: [Errno 4] Interrupted system call' - # Just ignore. - pass - - # Wait for the real selector to be done. - th.join() - assert result is not None - return result - - def close(self) -> None: - """ - Clean up resources. - """ - if self._r: - os.close(self._r) - os.close(self._w) - - self._r = self._w = -1 - self.selector.close() - - def get_map(self) -> Mapping["FileDescriptorLike", "SelectorKey"]: - return self.selector.get_map() - - -class InputHookContext: - """ - Given as a parameter to the inputhook. - """ - - def __init__(self, fileno: int, input_is_ready: Callable[[], bool]) -> None: - self._fileno = fileno - self.input_is_ready = input_is_ready - - def fileno(self) -> int: - return self._fileno + + _EventMask = int + + +def new_eventloop_with_inputhook( + inputhook: Callable[["InputHookContext"], None] +) -> AbstractEventLoop: + """ + Create a new event loop with the given inputhook. + """ + selector = InputHookSelector(selectors.DefaultSelector(), inputhook) + loop = asyncio.SelectorEventLoop(selector) + return loop + + +def set_eventloop_with_inputhook( + inputhook: Callable[["InputHookContext"], None] +) -> AbstractEventLoop: + """ + Create a new event loop with the given inputhook, and activate it. + """ + loop = new_eventloop_with_inputhook(inputhook) + asyncio.set_event_loop(loop) + return loop + + +class InputHookSelector(BaseSelector): + """ + Usage: + + selector = selectors.SelectSelector() + loop = asyncio.SelectorEventLoop(InputHookSelector(selector, inputhook)) + asyncio.set_event_loop(loop) + """ + + def __init__( + self, selector: BaseSelector, inputhook: Callable[["InputHookContext"], None] + ) -> None: + self.selector = selector + self.inputhook = inputhook + self._r, self._w = os.pipe() + + def register( + self, fileobj: "FileDescriptorLike", events: "_EventMask", data: Any = None + ) -> "SelectorKey": + return self.selector.register(fileobj, events, data=data) + + def unregister(self, fileobj: "FileDescriptorLike") -> "SelectorKey": + return self.selector.unregister(fileobj) + + def modify( + self, fileobj: "FileDescriptorLike", events: "_EventMask", data: Any = None + ) -> "SelectorKey": + return self.selector.modify(fileobj, events, data=None) + + def select( + self, timeout: Optional[float] = None + ) -> List[Tuple["SelectorKey", "_EventMask"]]: + # If there are tasks in the current event loop, + # don't run the input hook. + if len(getattr(get_event_loop(), "_ready", [])) > 0: + return self.selector.select(timeout=timeout) + + ready = False + result = None + + # Run selector in other thread. + def run_selector() -> None: + nonlocal ready, result + result = self.selector.select(timeout=timeout) + os.write(self._w, b"x") + ready = True + + th = threading.Thread(target=run_selector) + th.start() + + def input_is_ready() -> bool: + return ready + + # Call inputhook. + # The inputhook function is supposed to return when our selector + # becomes ready. The inputhook can do that by registering the fd in its + # own loop, or by checking the `input_is_ready` function regularly. + self.inputhook(InputHookContext(self._r, input_is_ready)) + + # Flush the read end of the pipe. + try: + # Before calling 'os.read', call select.select. This is required + # when the gevent monkey patch has been applied. 'os.read' is never + # monkey patched and won't be cooperative, so that would block all + # other select() calls otherwise. + # See: http://www.gevent.org/gevent.os.html + + # Note: On Windows, this is apparently not an issue. + # However, if we would ever want to add a select call, it + # should use `windll.kernel32.WaitForMultipleObjects`, + # because `select.select` can't wait for a pipe on Windows. + if not is_windows(): + select.select([self._r], [], [], None) + + os.read(self._r, 1024) + except OSError: + # This happens when the window resizes and a SIGWINCH was received. + # We get 'Error: [Errno 4] Interrupted system call' + # Just ignore. + pass + + # Wait for the real selector to be done. + th.join() + assert result is not None + return result + + def close(self) -> None: + """ + Clean up resources. + """ + if self._r: + os.close(self._r) + os.close(self._w) + + self._r = self._w = -1 + self.selector.close() + + def get_map(self) -> Mapping["FileDescriptorLike", "SelectorKey"]: + return self.selector.get_map() + + +class InputHookContext: + """ + Given as a parameter to the inputhook. + """ + + def __init__(self, fileno: int, input_is_ready: Callable[[], bool]) -> None: + self._fileno = fileno + self.input_is_ready = input_is_ready + + def fileno(self) -> int: + return self._fileno diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/utils.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/utils.py index f9809e42b2..2e5a05e838 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/utils.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/utils.py @@ -1,118 +1,118 @@ -import asyncio -import sys -import time -from types import TracebackType -from typing import Any, Awaitable, Callable, Dict, Optional, TypeVar, cast - -try: - import contextvars -except ImportError: - from . import dummy_contextvars as contextvars # type: ignore - -__all__ = [ - "run_in_executor_with_context", - "call_soon_threadsafe", - "get_traceback_from_context", - "get_event_loop", -] - -_T = TypeVar("_T") - - -def run_in_executor_with_context( - func: Callable[..., _T], - *args: Any, - loop: Optional[asyncio.AbstractEventLoop] = None, -) -> Awaitable[_T]: - """ - Run a function in an executor, but make sure it uses the same contextvars. - This is required so that the function will see the right application. - - See also: https://bugs.python.org/issue34014 - """ - loop = loop or get_event_loop() - ctx: contextvars.Context = contextvars.copy_context() - - return loop.run_in_executor(None, ctx.run, func, *args) - - -def call_soon_threadsafe( - func: Callable[[], None], - max_postpone_time: Optional[float] = None, - loop: Optional[asyncio.AbstractEventLoop] = None, -) -> None: - """ - Wrapper around asyncio's `call_soon_threadsafe`. - - This takes a `max_postpone_time` which can be used to tune the urgency of - the method. - - Asyncio runs tasks in first-in-first-out. However, this is not what we - want for the render function of the prompt_toolkit UI. Rendering is - expensive, but since the UI is invalidated very often, in some situations - we render the UI too often, so much that the rendering CPU usage slows down - the rest of the processing of the application. (Pymux is an example where - we have to balance the CPU time spend on rendering the UI, and parsing - process output.) - However, we want to set a deadline value, for when the rendering should - happen. (The UI should stay responsive). - """ - loop2 = loop or get_event_loop() - - # If no `max_postpone_time` has been given, schedule right now. - if max_postpone_time is None: - loop2.call_soon_threadsafe(func) - return - - max_postpone_until = time.time() + max_postpone_time - - def schedule() -> None: - # When there are no other tasks scheduled in the event loop. Run it - # now. - # Notice: uvloop doesn't have this _ready attribute. In that case, - # always call immediately. - if not getattr(loop2, "_ready", []): - func() - return - - # If the timeout expired, run this now. - if time.time() > max_postpone_until: - func() - return - - # Schedule again for later. - loop2.call_soon_threadsafe(schedule) - - loop2.call_soon_threadsafe(schedule) - - -def get_traceback_from_context(context: Dict[str, Any]) -> Optional[TracebackType]: - """ - Get the traceback object from the context. - """ - exception = context.get("exception") - if exception: - if hasattr(exception, "__traceback__"): - return cast(TracebackType, exception.__traceback__) - else: - # call_exception_handler() is usually called indirectly - # from an except block. If it's not the case, the traceback - # is undefined... - return sys.exc_info()[2] - - return None - - -def get_event_loop() -> asyncio.AbstractEventLoop: - """Backward compatible way to get the event loop""" - # Python 3.6 doesn't have get_running_loop - # Python 3.10 deprecated get_event_loop - if sys.version_info >= (3, 7): - getloop = asyncio.get_running_loop - else: - getloop = asyncio.get_event_loop - - try: - return getloop() - except RuntimeError: - return asyncio.get_event_loop_policy().get_event_loop() +import asyncio +import sys +import time +from types import TracebackType +from typing import Any, Awaitable, Callable, Dict, Optional, TypeVar, cast + +try: + import contextvars +except ImportError: + from . import dummy_contextvars as contextvars # type: ignore + +__all__ = [ + "run_in_executor_with_context", + "call_soon_threadsafe", + "get_traceback_from_context", + "get_event_loop", +] + +_T = TypeVar("_T") + + +def run_in_executor_with_context( + func: Callable[..., _T], + *args: Any, + loop: Optional[asyncio.AbstractEventLoop] = None, +) -> Awaitable[_T]: + """ + Run a function in an executor, but make sure it uses the same contextvars. + This is required so that the function will see the right application. + + See also: https://bugs.python.org/issue34014 + """ + loop = loop or get_event_loop() + ctx: contextvars.Context = contextvars.copy_context() + + return loop.run_in_executor(None, ctx.run, func, *args) + + +def call_soon_threadsafe( + func: Callable[[], None], + max_postpone_time: Optional[float] = None, + loop: Optional[asyncio.AbstractEventLoop] = None, +) -> None: + """ + Wrapper around asyncio's `call_soon_threadsafe`. + + This takes a `max_postpone_time` which can be used to tune the urgency of + the method. + + Asyncio runs tasks in first-in-first-out. However, this is not what we + want for the render function of the prompt_toolkit UI. Rendering is + expensive, but since the UI is invalidated very often, in some situations + we render the UI too often, so much that the rendering CPU usage slows down + the rest of the processing of the application. (Pymux is an example where + we have to balance the CPU time spend on rendering the UI, and parsing + process output.) + However, we want to set a deadline value, for when the rendering should + happen. (The UI should stay responsive). + """ + loop2 = loop or get_event_loop() + + # If no `max_postpone_time` has been given, schedule right now. + if max_postpone_time is None: + loop2.call_soon_threadsafe(func) + return + + max_postpone_until = time.time() + max_postpone_time + + def schedule() -> None: + # When there are no other tasks scheduled in the event loop. Run it + # now. + # Notice: uvloop doesn't have this _ready attribute. In that case, + # always call immediately. + if not getattr(loop2, "_ready", []): + func() + return + + # If the timeout expired, run this now. + if time.time() > max_postpone_until: + func() + return + + # Schedule again for later. + loop2.call_soon_threadsafe(schedule) + + loop2.call_soon_threadsafe(schedule) + + +def get_traceback_from_context(context: Dict[str, Any]) -> Optional[TracebackType]: + """ + Get the traceback object from the context. + """ + exception = context.get("exception") + if exception: + if hasattr(exception, "__traceback__"): + return cast(TracebackType, exception.__traceback__) + else: + # call_exception_handler() is usually called indirectly + # from an except block. If it's not the case, the traceback + # is undefined... + return sys.exc_info()[2] + + return None + + +def get_event_loop() -> asyncio.AbstractEventLoop: + """Backward compatible way to get the event loop""" + # Python 3.6 doesn't have get_running_loop + # Python 3.10 deprecated get_event_loop + if sys.version_info >= (3, 7): + getloop = asyncio.get_running_loop + else: + getloop = asyncio.get_event_loop + + try: + return getloop() + except RuntimeError: + return asyncio.get_event_loop_policy().get_event_loop() diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/win32.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/win32.py index 63bba6db15..a53632e0e8 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/win32.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/eventloop/win32.py @@ -1,69 +1,69 @@ -from ctypes import pointer - -from ..utils import SPHINX_AUTODOC_RUNNING - -# Do not import win32-specific stuff when generating documentation. -# Otherwise RTD would be unable to generate docs for this module. -if not SPHINX_AUTODOC_RUNNING: - from ctypes import windll - -from ctypes.wintypes import BOOL, DWORD, HANDLE -from typing import List, Optional - -from prompt_toolkit.win32_types import SECURITY_ATTRIBUTES - -__all__ = ["wait_for_handles", "create_win32_event"] - - -WAIT_TIMEOUT = 0x00000102 -INFINITE = -1 - - -def wait_for_handles( - handles: List[HANDLE], timeout: int = INFINITE -) -> Optional[HANDLE]: - """ - Waits for multiple handles. (Similar to 'select') Returns the handle which is ready. - Returns `None` on timeout. - http://msdn.microsoft.com/en-us/library/windows/desktop/ms687025(v=vs.85).aspx - - Note that handles should be a list of `HANDLE` objects, not integers. See - this comment in the patch by @quark-zju for the reason why: - - ''' Make sure HANDLE on Windows has a correct size - - Previously, the type of various HANDLEs are native Python integer - types. The ctypes library will treat them as 4-byte integer when used - in function arguments. On 64-bit Windows, HANDLE is 8-byte and usually - a small integer. Depending on whether the extra 4 bytes are zero-ed out - or not, things can happen to work, or break. ''' - - This function returns either `None` or one of the given `HANDLE` objects. - (The return value can be tested with the `is` operator.) - """ - arrtype = HANDLE * len(handles) - handle_array = arrtype(*handles) - - ret: int = windll.kernel32.WaitForMultipleObjects( - len(handle_array), handle_array, BOOL(False), DWORD(timeout) - ) - - if ret == WAIT_TIMEOUT: - return None - else: - return handles[ret] - - -def create_win32_event() -> HANDLE: - """ - Creates a Win32 unnamed Event . - http://msdn.microsoft.com/en-us/library/windows/desktop/ms682396(v=vs.85).aspx - """ - return HANDLE( - windll.kernel32.CreateEventA( - pointer(SECURITY_ATTRIBUTES()), - BOOL(True), # Manual reset event. - BOOL(False), # Initial state. - None, # Unnamed event object. - ) - ) +from ctypes import pointer + +from ..utils import SPHINX_AUTODOC_RUNNING + +# Do not import win32-specific stuff when generating documentation. +# Otherwise RTD would be unable to generate docs for this module. +if not SPHINX_AUTODOC_RUNNING: + from ctypes import windll + +from ctypes.wintypes import BOOL, DWORD, HANDLE +from typing import List, Optional + +from prompt_toolkit.win32_types import SECURITY_ATTRIBUTES + +__all__ = ["wait_for_handles", "create_win32_event"] + + +WAIT_TIMEOUT = 0x00000102 +INFINITE = -1 + + +def wait_for_handles( + handles: List[HANDLE], timeout: int = INFINITE +) -> Optional[HANDLE]: + """ + Waits for multiple handles. (Similar to 'select') Returns the handle which is ready. + Returns `None` on timeout. + http://msdn.microsoft.com/en-us/library/windows/desktop/ms687025(v=vs.85).aspx + + Note that handles should be a list of `HANDLE` objects, not integers. See + this comment in the patch by @quark-zju for the reason why: + + ''' Make sure HANDLE on Windows has a correct size + + Previously, the type of various HANDLEs are native Python integer + types. The ctypes library will treat them as 4-byte integer when used + in function arguments. On 64-bit Windows, HANDLE is 8-byte and usually + a small integer. Depending on whether the extra 4 bytes are zero-ed out + or not, things can happen to work, or break. ''' + + This function returns either `None` or one of the given `HANDLE` objects. + (The return value can be tested with the `is` operator.) + """ + arrtype = HANDLE * len(handles) + handle_array = arrtype(*handles) + + ret: int = windll.kernel32.WaitForMultipleObjects( + len(handle_array), handle_array, BOOL(False), DWORD(timeout) + ) + + if ret == WAIT_TIMEOUT: + return None + else: + return handles[ret] + + +def create_win32_event() -> HANDLE: + """ + Creates a Win32 unnamed Event . + http://msdn.microsoft.com/en-us/library/windows/desktop/ms682396(v=vs.85).aspx + """ + return HANDLE( + windll.kernel32.CreateEventA( + pointer(SECURITY_ATTRIBUTES()), + BOOL(True), # Manual reset event. + BOOL(False), # Initial state. + None, # Unnamed event object. + ) + ) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/filters/__init__.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/filters/__init__.py index 5692c30acd..d97195fb82 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/filters/__init__.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/filters/__init__.py @@ -1,68 +1,68 @@ -""" -Filters decide whether something is active or not (they decide about a boolean -state). This is used to enable/disable features, like key bindings, parts of -the layout and other stuff. For instance, we could have a `HasSearch` filter -attached to some part of the layout, in order to show that part of the user -interface only while the user is searching. - -Filters are made to avoid having to attach callbacks to all event in order to -propagate state. However, they are lazy, they don't automatically propagate the -state of what they are observing. Only when a filter is called (it's actually a -callable), it will calculate its value. So, its not really reactive -programming, but it's made to fit for this framework. - -Filters can be chained using ``&`` and ``|`` operations, and inverted using the -``~`` operator, for instance:: - - filter = has_focus('default') & ~ has_selection -""" -from .app import * -from .base import Always, Condition, Filter, FilterOrBool, Never -from .cli import * -from .utils import is_true, to_filter - -__all__ = [ - # app - "has_arg", - "has_completions", - "completion_is_selected", - "has_focus", - "buffer_has_focus", - "has_selection", - "has_validation_error", - "is_done", - "is_read_only", - "is_multiline", - "renderer_height_is_known", - "in_editing_mode", - "in_paste_mode", - "vi_mode", - "vi_navigation_mode", - "vi_insert_mode", - "vi_insert_multiple_mode", - "vi_replace_mode", - "vi_selection_mode", - "vi_waiting_for_text_object_mode", - "vi_digraph_mode", - "vi_recording_macro", - "emacs_mode", - "emacs_insert_mode", - "emacs_selection_mode", - "shift_selection_mode", - "is_searching", - "control_is_searchable", - "vi_search_direction_reversed", - # base. - "Filter", - "Never", - "Always", - "Condition", - "FilterOrBool", - # utils. - "is_true", - "to_filter", -] - -from .cli import __all__ as cli_all - -__all__.extend(cli_all) +""" +Filters decide whether something is active or not (they decide about a boolean +state). This is used to enable/disable features, like key bindings, parts of +the layout and other stuff. For instance, we could have a `HasSearch` filter +attached to some part of the layout, in order to show that part of the user +interface only while the user is searching. + +Filters are made to avoid having to attach callbacks to all event in order to +propagate state. However, they are lazy, they don't automatically propagate the +state of what they are observing. Only when a filter is called (it's actually a +callable), it will calculate its value. So, its not really reactive +programming, but it's made to fit for this framework. + +Filters can be chained using ``&`` and ``|`` operations, and inverted using the +``~`` operator, for instance:: + + filter = has_focus('default') & ~ has_selection +""" +from .app import * +from .base import Always, Condition, Filter, FilterOrBool, Never +from .cli import * +from .utils import is_true, to_filter + +__all__ = [ + # app + "has_arg", + "has_completions", + "completion_is_selected", + "has_focus", + "buffer_has_focus", + "has_selection", + "has_validation_error", + "is_done", + "is_read_only", + "is_multiline", + "renderer_height_is_known", + "in_editing_mode", + "in_paste_mode", + "vi_mode", + "vi_navigation_mode", + "vi_insert_mode", + "vi_insert_multiple_mode", + "vi_replace_mode", + "vi_selection_mode", + "vi_waiting_for_text_object_mode", + "vi_digraph_mode", + "vi_recording_macro", + "emacs_mode", + "emacs_insert_mode", + "emacs_selection_mode", + "shift_selection_mode", + "is_searching", + "control_is_searchable", + "vi_search_direction_reversed", + # base. + "Filter", + "Never", + "Always", + "Condition", + "FilterOrBool", + # utils. + "is_true", + "to_filter", +] + +from .cli import __all__ as cli_all + +__all__.extend(cli_all) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/filters/app.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/filters/app.py index 266313d0a4..767ec492d5 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/filters/app.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/filters/app.py @@ -1,402 +1,402 @@ -""" -Filters that accept a `Application` as argument. -""" -from typing import TYPE_CHECKING, cast - -from prompt_toolkit.application.current import get_app -from prompt_toolkit.cache import memoized -from prompt_toolkit.enums import EditingMode - -from .base import Condition - -if TYPE_CHECKING: - from prompt_toolkit.layout.layout import FocusableElement - - -__all__ = [ - "has_arg", - "has_completions", - "completion_is_selected", - "has_focus", - "buffer_has_focus", - "has_selection", - "has_validation_error", - "is_done", - "is_read_only", - "is_multiline", - "renderer_height_is_known", - "in_editing_mode", - "in_paste_mode", - "vi_mode", - "vi_navigation_mode", - "vi_insert_mode", - "vi_insert_multiple_mode", - "vi_replace_mode", - "vi_selection_mode", - "vi_waiting_for_text_object_mode", - "vi_digraph_mode", - "vi_recording_macro", - "emacs_mode", - "emacs_insert_mode", - "emacs_selection_mode", - "shift_selection_mode", - "is_searching", - "control_is_searchable", - "vi_search_direction_reversed", -] - - -@memoized() -def has_focus(value: "FocusableElement") -> Condition: - """ - Enable when this buffer has the focus. - """ - from prompt_toolkit.buffer import Buffer - from prompt_toolkit.layout import walk - from prompt_toolkit.layout.containers import Container, Window, to_container - from prompt_toolkit.layout.controls import UIControl - - if isinstance(value, str): - - def test() -> bool: - return get_app().current_buffer.name == value - - elif isinstance(value, Buffer): - - def test() -> bool: - return get_app().current_buffer == value - - elif isinstance(value, UIControl): - - def test() -> bool: - return get_app().layout.current_control == value - - else: - value = to_container(value) - - if isinstance(value, Window): - - def test() -> bool: - return get_app().layout.current_window == value - - else: - - def test() -> bool: - # Consider focused when any window inside this container is - # focused. - current_window = get_app().layout.current_window - - for c in walk(cast(Container, value)): - if isinstance(c, Window) and c == current_window: - return True - return False - - @Condition - def has_focus_filter() -> bool: - return test() - - return has_focus_filter - - -@Condition -def buffer_has_focus() -> bool: - """ - Enabled when the currently focused control is a `BufferControl`. - """ - return get_app().layout.buffer_has_focus - - -@Condition -def has_selection() -> bool: - """ - Enable when the current buffer has a selection. - """ - return bool(get_app().current_buffer.selection_state) - - -@Condition -def has_completions() -> bool: - """ - Enable when the current buffer has completions. - """ - state = get_app().current_buffer.complete_state - return state is not None and len(state.completions) > 0 - - -@Condition -def completion_is_selected() -> bool: - """ - True when the user selected a completion. - """ - complete_state = get_app().current_buffer.complete_state - return complete_state is not None and complete_state.current_completion is not None - - -@Condition -def is_read_only() -> bool: - """ - True when the current buffer is read only. - """ - return get_app().current_buffer.read_only() - - -@Condition -def is_multiline() -> bool: - """ - True when the current buffer has been marked as multiline. - """ - return get_app().current_buffer.multiline() - - -@Condition -def has_validation_error() -> bool: - "Current buffer has validation error." - return get_app().current_buffer.validation_error is not None - - -@Condition -def has_arg() -> bool: - "Enable when the input processor has an 'arg'." - return get_app().key_processor.arg is not None - - -@Condition -def is_done() -> bool: - """ - True when the CLI is returning, aborting or exiting. - """ - return get_app().is_done - - -@Condition -def renderer_height_is_known() -> bool: - """ - Only True when the renderer knows it's real height. - - (On VT100 terminals, we have to wait for a CPR response, before we can be - sure of the available height between the cursor position and the bottom of - the terminal. And usually it's nicer to wait with drawing bottom toolbars - until we receive the height, in order to avoid flickering -- first drawing - somewhere in the middle, and then again at the bottom.) - """ - return get_app().renderer.height_is_known - - -@memoized() -def in_editing_mode(editing_mode: EditingMode) -> Condition: - """ - Check whether a given editing mode is active. (Vi or Emacs.) - """ - - @Condition - def in_editing_mode_filter() -> bool: - return get_app().editing_mode == editing_mode - - return in_editing_mode_filter - - -@Condition -def in_paste_mode() -> bool: - return get_app().paste_mode() - - -@Condition -def vi_mode() -> bool: - return get_app().editing_mode == EditingMode.VI - - -@Condition -def vi_navigation_mode() -> bool: - """ - Active when the set for Vi navigation key bindings are active. - """ - from prompt_toolkit.key_binding.vi_state import InputMode - - app = get_app() - - if ( - app.editing_mode != EditingMode.VI - or app.vi_state.operator_func - or app.vi_state.waiting_for_digraph - or app.current_buffer.selection_state - ): - return False - - return ( - app.vi_state.input_mode == InputMode.NAVIGATION - or app.vi_state.temporary_navigation_mode - or app.current_buffer.read_only() - ) - - -@Condition -def vi_insert_mode() -> bool: - from prompt_toolkit.key_binding.vi_state import InputMode - - app = get_app() - - if ( - app.editing_mode != EditingMode.VI - or app.vi_state.operator_func - or app.vi_state.waiting_for_digraph - or app.current_buffer.selection_state - or app.vi_state.temporary_navigation_mode - or app.current_buffer.read_only() - ): - return False - - return app.vi_state.input_mode == InputMode.INSERT - - -@Condition -def vi_insert_multiple_mode() -> bool: - from prompt_toolkit.key_binding.vi_state import InputMode - - app = get_app() - - if ( - app.editing_mode != EditingMode.VI - or app.vi_state.operator_func - or app.vi_state.waiting_for_digraph - or app.current_buffer.selection_state - or app.vi_state.temporary_navigation_mode - or app.current_buffer.read_only() - ): - return False - - return app.vi_state.input_mode == InputMode.INSERT_MULTIPLE - - -@Condition -def vi_replace_mode() -> bool: - from prompt_toolkit.key_binding.vi_state import InputMode - - app = get_app() - - if ( - app.editing_mode != EditingMode.VI - or app.vi_state.operator_func - or app.vi_state.waiting_for_digraph - or app.current_buffer.selection_state - or app.vi_state.temporary_navigation_mode - or app.current_buffer.read_only() - ): - return False - - return app.vi_state.input_mode == InputMode.REPLACE - - -@Condition -def vi_replace_single_mode() -> bool: - from prompt_toolkit.key_binding.vi_state import InputMode - - app = get_app() - - if ( - app.editing_mode != EditingMode.VI - or app.vi_state.operator_func - or app.vi_state.waiting_for_digraph - or app.current_buffer.selection_state - or app.vi_state.temporary_navigation_mode - or app.current_buffer.read_only() - ): - return False - - return app.vi_state.input_mode == InputMode.REPLACE_SINGLE - - -@Condition -def vi_selection_mode() -> bool: - app = get_app() - if app.editing_mode != EditingMode.VI: - return False - - return bool(app.current_buffer.selection_state) - - -@Condition -def vi_waiting_for_text_object_mode() -> bool: - app = get_app() - if app.editing_mode != EditingMode.VI: - return False - - return app.vi_state.operator_func is not None - - -@Condition -def vi_digraph_mode() -> bool: - app = get_app() - if app.editing_mode != EditingMode.VI: - return False - - return app.vi_state.waiting_for_digraph - - -@Condition -def vi_recording_macro() -> bool: - "When recording a Vi macro." - app = get_app() - if app.editing_mode != EditingMode.VI: - return False - - return app.vi_state.recording_register is not None - - -@Condition -def emacs_mode() -> bool: - "When the Emacs bindings are active." - return get_app().editing_mode == EditingMode.EMACS - - -@Condition -def emacs_insert_mode() -> bool: - app = get_app() - if ( - app.editing_mode != EditingMode.EMACS - or app.current_buffer.selection_state - or app.current_buffer.read_only() - ): - return False - return True - - -@Condition -def emacs_selection_mode() -> bool: - app = get_app() - return bool( - app.editing_mode == EditingMode.EMACS and app.current_buffer.selection_state - ) - - -@Condition -def shift_selection_mode() -> bool: - app = get_app() - return bool( - app.current_buffer.selection_state - and app.current_buffer.selection_state.shift_mode - ) - - -@Condition -def is_searching() -> bool: - "When we are searching." - app = get_app() - return app.layout.is_searching - - -@Condition -def control_is_searchable() -> bool: - "When the current UIControl is searchable." - from prompt_toolkit.layout.controls import BufferControl - - control = get_app().layout.current_control - - return ( - isinstance(control, BufferControl) and control.search_buffer_control is not None - ) - - -@Condition -def vi_search_direction_reversed() -> bool: - "When the '/' and '?' key bindings for Vi-style searching have been reversed." - return get_app().reverse_vi_search_direction() +""" +Filters that accept a `Application` as argument. +""" +from typing import TYPE_CHECKING, cast + +from prompt_toolkit.application.current import get_app +from prompt_toolkit.cache import memoized +from prompt_toolkit.enums import EditingMode + +from .base import Condition + +if TYPE_CHECKING: + from prompt_toolkit.layout.layout import FocusableElement + + +__all__ = [ + "has_arg", + "has_completions", + "completion_is_selected", + "has_focus", + "buffer_has_focus", + "has_selection", + "has_validation_error", + "is_done", + "is_read_only", + "is_multiline", + "renderer_height_is_known", + "in_editing_mode", + "in_paste_mode", + "vi_mode", + "vi_navigation_mode", + "vi_insert_mode", + "vi_insert_multiple_mode", + "vi_replace_mode", + "vi_selection_mode", + "vi_waiting_for_text_object_mode", + "vi_digraph_mode", + "vi_recording_macro", + "emacs_mode", + "emacs_insert_mode", + "emacs_selection_mode", + "shift_selection_mode", + "is_searching", + "control_is_searchable", + "vi_search_direction_reversed", +] + + +@memoized() +def has_focus(value: "FocusableElement") -> Condition: + """ + Enable when this buffer has the focus. + """ + from prompt_toolkit.buffer import Buffer + from prompt_toolkit.layout import walk + from prompt_toolkit.layout.containers import Container, Window, to_container + from prompt_toolkit.layout.controls import UIControl + + if isinstance(value, str): + + def test() -> bool: + return get_app().current_buffer.name == value + + elif isinstance(value, Buffer): + + def test() -> bool: + return get_app().current_buffer == value + + elif isinstance(value, UIControl): + + def test() -> bool: + return get_app().layout.current_control == value + + else: + value = to_container(value) + + if isinstance(value, Window): + + def test() -> bool: + return get_app().layout.current_window == value + + else: + + def test() -> bool: + # Consider focused when any window inside this container is + # focused. + current_window = get_app().layout.current_window + + for c in walk(cast(Container, value)): + if isinstance(c, Window) and c == current_window: + return True + return False + + @Condition + def has_focus_filter() -> bool: + return test() + + return has_focus_filter + + +@Condition +def buffer_has_focus() -> bool: + """ + Enabled when the currently focused control is a `BufferControl`. + """ + return get_app().layout.buffer_has_focus + + +@Condition +def has_selection() -> bool: + """ + Enable when the current buffer has a selection. + """ + return bool(get_app().current_buffer.selection_state) + + +@Condition +def has_completions() -> bool: + """ + Enable when the current buffer has completions. + """ + state = get_app().current_buffer.complete_state + return state is not None and len(state.completions) > 0 + + +@Condition +def completion_is_selected() -> bool: + """ + True when the user selected a completion. + """ + complete_state = get_app().current_buffer.complete_state + return complete_state is not None and complete_state.current_completion is not None + + +@Condition +def is_read_only() -> bool: + """ + True when the current buffer is read only. + """ + return get_app().current_buffer.read_only() + + +@Condition +def is_multiline() -> bool: + """ + True when the current buffer has been marked as multiline. + """ + return get_app().current_buffer.multiline() + + +@Condition +def has_validation_error() -> bool: + "Current buffer has validation error." + return get_app().current_buffer.validation_error is not None + + +@Condition +def has_arg() -> bool: + "Enable when the input processor has an 'arg'." + return get_app().key_processor.arg is not None + + +@Condition +def is_done() -> bool: + """ + True when the CLI is returning, aborting or exiting. + """ + return get_app().is_done + + +@Condition +def renderer_height_is_known() -> bool: + """ + Only True when the renderer knows it's real height. + + (On VT100 terminals, we have to wait for a CPR response, before we can be + sure of the available height between the cursor position and the bottom of + the terminal. And usually it's nicer to wait with drawing bottom toolbars + until we receive the height, in order to avoid flickering -- first drawing + somewhere in the middle, and then again at the bottom.) + """ + return get_app().renderer.height_is_known + + +@memoized() +def in_editing_mode(editing_mode: EditingMode) -> Condition: + """ + Check whether a given editing mode is active. (Vi or Emacs.) + """ + + @Condition + def in_editing_mode_filter() -> bool: + return get_app().editing_mode == editing_mode + + return in_editing_mode_filter + + +@Condition +def in_paste_mode() -> bool: + return get_app().paste_mode() + + +@Condition +def vi_mode() -> bool: + return get_app().editing_mode == EditingMode.VI + + +@Condition +def vi_navigation_mode() -> bool: + """ + Active when the set for Vi navigation key bindings are active. + """ + from prompt_toolkit.key_binding.vi_state import InputMode + + app = get_app() + + if ( + app.editing_mode != EditingMode.VI + or app.vi_state.operator_func + or app.vi_state.waiting_for_digraph + or app.current_buffer.selection_state + ): + return False + + return ( + app.vi_state.input_mode == InputMode.NAVIGATION + or app.vi_state.temporary_navigation_mode + or app.current_buffer.read_only() + ) + + +@Condition +def vi_insert_mode() -> bool: + from prompt_toolkit.key_binding.vi_state import InputMode + + app = get_app() + + if ( + app.editing_mode != EditingMode.VI + or app.vi_state.operator_func + or app.vi_state.waiting_for_digraph + or app.current_buffer.selection_state + or app.vi_state.temporary_navigation_mode + or app.current_buffer.read_only() + ): + return False + + return app.vi_state.input_mode == InputMode.INSERT + + +@Condition +def vi_insert_multiple_mode() -> bool: + from prompt_toolkit.key_binding.vi_state import InputMode + + app = get_app() + + if ( + app.editing_mode != EditingMode.VI + or app.vi_state.operator_func + or app.vi_state.waiting_for_digraph + or app.current_buffer.selection_state + or app.vi_state.temporary_navigation_mode + or app.current_buffer.read_only() + ): + return False + + return app.vi_state.input_mode == InputMode.INSERT_MULTIPLE + + +@Condition +def vi_replace_mode() -> bool: + from prompt_toolkit.key_binding.vi_state import InputMode + + app = get_app() + + if ( + app.editing_mode != EditingMode.VI + or app.vi_state.operator_func + or app.vi_state.waiting_for_digraph + or app.current_buffer.selection_state + or app.vi_state.temporary_navigation_mode + or app.current_buffer.read_only() + ): + return False + + return app.vi_state.input_mode == InputMode.REPLACE + + +@Condition +def vi_replace_single_mode() -> bool: + from prompt_toolkit.key_binding.vi_state import InputMode + + app = get_app() + + if ( + app.editing_mode != EditingMode.VI + or app.vi_state.operator_func + or app.vi_state.waiting_for_digraph + or app.current_buffer.selection_state + or app.vi_state.temporary_navigation_mode + or app.current_buffer.read_only() + ): + return False + + return app.vi_state.input_mode == InputMode.REPLACE_SINGLE + + +@Condition +def vi_selection_mode() -> bool: + app = get_app() + if app.editing_mode != EditingMode.VI: + return False + + return bool(app.current_buffer.selection_state) + + +@Condition +def vi_waiting_for_text_object_mode() -> bool: + app = get_app() + if app.editing_mode != EditingMode.VI: + return False + + return app.vi_state.operator_func is not None + + +@Condition +def vi_digraph_mode() -> bool: + app = get_app() + if app.editing_mode != EditingMode.VI: + return False + + return app.vi_state.waiting_for_digraph + + +@Condition +def vi_recording_macro() -> bool: + "When recording a Vi macro." + app = get_app() + if app.editing_mode != EditingMode.VI: + return False + + return app.vi_state.recording_register is not None + + +@Condition +def emacs_mode() -> bool: + "When the Emacs bindings are active." + return get_app().editing_mode == EditingMode.EMACS + + +@Condition +def emacs_insert_mode() -> bool: + app = get_app() + if ( + app.editing_mode != EditingMode.EMACS + or app.current_buffer.selection_state + or app.current_buffer.read_only() + ): + return False + return True + + +@Condition +def emacs_selection_mode() -> bool: + app = get_app() + return bool( + app.editing_mode == EditingMode.EMACS and app.current_buffer.selection_state + ) + + +@Condition +def shift_selection_mode() -> bool: + app = get_app() + return bool( + app.current_buffer.selection_state + and app.current_buffer.selection_state.shift_mode + ) + + +@Condition +def is_searching() -> bool: + "When we are searching." + app = get_app() + return app.layout.is_searching + + +@Condition +def control_is_searchable() -> bool: + "When the current UIControl is searchable." + from prompt_toolkit.layout.controls import BufferControl + + control = get_app().layout.current_control + + return ( + isinstance(control, BufferControl) and control.search_buffer_control is not None + ) + + +@Condition +def vi_search_direction_reversed() -> bool: + "When the '/' and '?' key bindings for Vi-style searching have been reversed." + return get_app().reverse_vi_search_direction() diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/filters/base.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/filters/base.py index d50c2aa393..fd57cca6e9 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/filters/base.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/filters/base.py @@ -1,217 +1,217 @@ -from abc import ABCMeta, abstractmethod +from abc import ABCMeta, abstractmethod from typing import Callable, Dict, Iterable, List, Tuple, Union - -__all__ = ["Filter", "Never", "Always", "Condition", "FilterOrBool"] - - -class Filter(metaclass=ABCMeta): - """ - Base class for any filter to activate/deactivate a feature, depending on a - condition. - - The return value of ``__call__`` will tell if the feature should be active. - """ - - @abstractmethod - def __call__(self) -> bool: - """ - The actual call to evaluate the filter. - """ - return True - - def __and__(self, other: "Filter") -> "Filter": - """ - Chaining of filters using the & operator. - """ - return _and_cache[self, other] - - def __or__(self, other: "Filter") -> "Filter": - """ - Chaining of filters using the | operator. - """ - return _or_cache[self, other] - - def __invert__(self) -> "Filter": - """ - Inverting of filters using the ~ operator. - """ - return _invert_cache[self] - - def __bool__(self) -> None: - """ - By purpose, we don't allow bool(...) operations directly on a filter, - because the meaning is ambiguous. - - Executing a filter has to be done always by calling it. Providing - defaults for `None` values should be done through an `is None` check - instead of for instance ``filter1 or Always()``. - """ - raise ValueError( - "The truth value of a Filter is ambiguous. " - "Instead, call it as a function." - ) - - -class _AndCache(Dict[Tuple[Filter, Filter], "_AndList"]): - """ - Cache for And operation between filters. - (Filter classes are stateless, so we can reuse them.) - - Note: This could be a memory leak if we keep creating filters at runtime. - If that is True, the filters should be weakreffed (not the tuple of - filters), and tuples should be removed when one of these filters is - removed. In practise however, there is a finite amount of filters. - """ - - def __missing__(self, filters: Tuple[Filter, Filter]) -> Filter: - a, b = filters - assert isinstance(b, Filter), "Expecting filter, got %r" % b - - if isinstance(b, Always) or isinstance(a, Never): - return a - elif isinstance(b, Never) or isinstance(a, Always): - return b - - result = _AndList(filters) - self[filters] = result - return result - - -class _OrCache(Dict[Tuple[Filter, Filter], "_OrList"]): - """Cache for Or operation between filters.""" - - def __missing__(self, filters: Tuple[Filter, Filter]) -> Filter: - a, b = filters - assert isinstance(b, Filter), "Expecting filter, got %r" % b - - if isinstance(b, Always) or isinstance(a, Never): - return b - elif isinstance(b, Never) or isinstance(a, Always): - return a - - result = _OrList(filters) - self[filters] = result - return result - - -class _InvertCache(Dict[Filter, "_Invert"]): - """Cache for inversion operator.""" - - def __missing__(self, filter: Filter) -> Filter: - result = _Invert(filter) - self[filter] = result - return result - - -_and_cache = _AndCache() -_or_cache = _OrCache() -_invert_cache = _InvertCache() - - -class _AndList(Filter): - """ - Result of &-operation between several filters. - """ - - def __init__(self, filters: Iterable[Filter]) -> None: - self.filters: List[Filter] = [] - - for f in filters: - if isinstance(f, _AndList): # Turn nested _AndLists into one. - self.filters.extend(f.filters) - else: - self.filters.append(f) - - def __call__(self) -> bool: - return all(f() for f in self.filters) - - def __repr__(self) -> str: - return "&".join(repr(f) for f in self.filters) - - -class _OrList(Filter): - """ - Result of |-operation between several filters. - """ - - def __init__(self, filters: Iterable[Filter]) -> None: - self.filters: List[Filter] = [] - - for f in filters: - if isinstance(f, _OrList): # Turn nested _OrLists into one. - self.filters.extend(f.filters) - else: - self.filters.append(f) - - def __call__(self) -> bool: - return any(f() for f in self.filters) - - def __repr__(self) -> str: - return "|".join(repr(f) for f in self.filters) - - -class _Invert(Filter): - """ - Negation of another filter. - """ - - def __init__(self, filter: Filter) -> None: - self.filter = filter - - def __call__(self) -> bool: - return not self.filter() - - def __repr__(self) -> str: - return "~%r" % self.filter - - -class Always(Filter): - """ - Always enable feature. - """ - - def __call__(self) -> bool: - return True - - def __invert__(self) -> "Never": - return Never() - - -class Never(Filter): - """ - Never enable feature. - """ - - def __call__(self) -> bool: - return False - - def __invert__(self) -> Always: - return Always() - - -class Condition(Filter): - """ - Turn any callable into a Filter. The callable is supposed to not take any - arguments. - - This can be used as a decorator:: - - @Condition - def feature_is_active(): # `feature_is_active` becomes a Filter. - return True - - :param func: Callable which takes no inputs and returns a boolean. - """ - - def __init__(self, func: Callable[[], bool]) -> None: - self.func = func - - def __call__(self) -> bool: - return self.func() - - def __repr__(self) -> str: - return "Condition(%r)" % self.func - - -# Often used as type annotation. -FilterOrBool = Union[Filter, bool] + +__all__ = ["Filter", "Never", "Always", "Condition", "FilterOrBool"] + + +class Filter(metaclass=ABCMeta): + """ + Base class for any filter to activate/deactivate a feature, depending on a + condition. + + The return value of ``__call__`` will tell if the feature should be active. + """ + + @abstractmethod + def __call__(self) -> bool: + """ + The actual call to evaluate the filter. + """ + return True + + def __and__(self, other: "Filter") -> "Filter": + """ + Chaining of filters using the & operator. + """ + return _and_cache[self, other] + + def __or__(self, other: "Filter") -> "Filter": + """ + Chaining of filters using the | operator. + """ + return _or_cache[self, other] + + def __invert__(self) -> "Filter": + """ + Inverting of filters using the ~ operator. + """ + return _invert_cache[self] + + def __bool__(self) -> None: + """ + By purpose, we don't allow bool(...) operations directly on a filter, + because the meaning is ambiguous. + + Executing a filter has to be done always by calling it. Providing + defaults for `None` values should be done through an `is None` check + instead of for instance ``filter1 or Always()``. + """ + raise ValueError( + "The truth value of a Filter is ambiguous. " + "Instead, call it as a function." + ) + + +class _AndCache(Dict[Tuple[Filter, Filter], "_AndList"]): + """ + Cache for And operation between filters. + (Filter classes are stateless, so we can reuse them.) + + Note: This could be a memory leak if we keep creating filters at runtime. + If that is True, the filters should be weakreffed (not the tuple of + filters), and tuples should be removed when one of these filters is + removed. In practise however, there is a finite amount of filters. + """ + + def __missing__(self, filters: Tuple[Filter, Filter]) -> Filter: + a, b = filters + assert isinstance(b, Filter), "Expecting filter, got %r" % b + + if isinstance(b, Always) or isinstance(a, Never): + return a + elif isinstance(b, Never) or isinstance(a, Always): + return b + + result = _AndList(filters) + self[filters] = result + return result + + +class _OrCache(Dict[Tuple[Filter, Filter], "_OrList"]): + """Cache for Or operation between filters.""" + + def __missing__(self, filters: Tuple[Filter, Filter]) -> Filter: + a, b = filters + assert isinstance(b, Filter), "Expecting filter, got %r" % b + + if isinstance(b, Always) or isinstance(a, Never): + return b + elif isinstance(b, Never) or isinstance(a, Always): + return a + + result = _OrList(filters) + self[filters] = result + return result + + +class _InvertCache(Dict[Filter, "_Invert"]): + """Cache for inversion operator.""" + + def __missing__(self, filter: Filter) -> Filter: + result = _Invert(filter) + self[filter] = result + return result + + +_and_cache = _AndCache() +_or_cache = _OrCache() +_invert_cache = _InvertCache() + + +class _AndList(Filter): + """ + Result of &-operation between several filters. + """ + + def __init__(self, filters: Iterable[Filter]) -> None: + self.filters: List[Filter] = [] + + for f in filters: + if isinstance(f, _AndList): # Turn nested _AndLists into one. + self.filters.extend(f.filters) + else: + self.filters.append(f) + + def __call__(self) -> bool: + return all(f() for f in self.filters) + + def __repr__(self) -> str: + return "&".join(repr(f) for f in self.filters) + + +class _OrList(Filter): + """ + Result of |-operation between several filters. + """ + + def __init__(self, filters: Iterable[Filter]) -> None: + self.filters: List[Filter] = [] + + for f in filters: + if isinstance(f, _OrList): # Turn nested _OrLists into one. + self.filters.extend(f.filters) + else: + self.filters.append(f) + + def __call__(self) -> bool: + return any(f() for f in self.filters) + + def __repr__(self) -> str: + return "|".join(repr(f) for f in self.filters) + + +class _Invert(Filter): + """ + Negation of another filter. + """ + + def __init__(self, filter: Filter) -> None: + self.filter = filter + + def __call__(self) -> bool: + return not self.filter() + + def __repr__(self) -> str: + return "~%r" % self.filter + + +class Always(Filter): + """ + Always enable feature. + """ + + def __call__(self) -> bool: + return True + + def __invert__(self) -> "Never": + return Never() + + +class Never(Filter): + """ + Never enable feature. + """ + + def __call__(self) -> bool: + return False + + def __invert__(self) -> Always: + return Always() + + +class Condition(Filter): + """ + Turn any callable into a Filter. The callable is supposed to not take any + arguments. + + This can be used as a decorator:: + + @Condition + def feature_is_active(): # `feature_is_active` becomes a Filter. + return True + + :param func: Callable which takes no inputs and returns a boolean. + """ + + def __init__(self, func: Callable[[], bool]) -> None: + self.func = func + + def __call__(self) -> bool: + return self.func() + + def __repr__(self) -> str: + return "Condition(%r)" % self.func + + +# Often used as type annotation. +FilterOrBool = Union[Filter, bool] diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/filters/cli.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/filters/cli.py index f4f38257d2..7135196cdd 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/filters/cli.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/filters/cli.py @@ -1,62 +1,62 @@ -""" -For backwards-compatibility. keep this file. -(Many people are going to have key bindings that rely on this file.) -""" -from .app import * - -__all__ = [ - # Old names. - "HasArg", - "HasCompletions", - "HasFocus", - "HasSelection", - "HasValidationError", - "IsDone", - "IsReadOnly", - "IsMultiline", - "RendererHeightIsKnown", - "InEditingMode", - "InPasteMode", - "ViMode", - "ViNavigationMode", - "ViInsertMode", - "ViInsertMultipleMode", - "ViReplaceMode", - "ViSelectionMode", - "ViWaitingForTextObjectMode", - "ViDigraphMode", - "EmacsMode", - "EmacsInsertMode", - "EmacsSelectionMode", - "IsSearching", - "HasSearch", - "ControlIsSearchable", -] - -# Keep the original classnames for backwards compatibility. -HasValidationError = lambda: has_validation_error -HasArg = lambda: has_arg -IsDone = lambda: is_done -RendererHeightIsKnown = lambda: renderer_height_is_known -ViNavigationMode = lambda: vi_navigation_mode -InPasteMode = lambda: in_paste_mode -EmacsMode = lambda: emacs_mode -EmacsInsertMode = lambda: emacs_insert_mode -ViMode = lambda: vi_mode -IsSearching = lambda: is_searching -HasSearch = lambda: is_searching -ControlIsSearchable = lambda: control_is_searchable -EmacsSelectionMode = lambda: emacs_selection_mode -ViDigraphMode = lambda: vi_digraph_mode -ViWaitingForTextObjectMode = lambda: vi_waiting_for_text_object_mode -ViSelectionMode = lambda: vi_selection_mode -ViReplaceMode = lambda: vi_replace_mode -ViInsertMultipleMode = lambda: vi_insert_multiple_mode -ViInsertMode = lambda: vi_insert_mode -HasSelection = lambda: has_selection -HasCompletions = lambda: has_completions -IsReadOnly = lambda: is_read_only -IsMultiline = lambda: is_multiline - -HasFocus = has_focus # No lambda here! (Has_focus is callable that returns a callable.) -InEditingMode = in_editing_mode +""" +For backwards-compatibility. keep this file. +(Many people are going to have key bindings that rely on this file.) +""" +from .app import * + +__all__ = [ + # Old names. + "HasArg", + "HasCompletions", + "HasFocus", + "HasSelection", + "HasValidationError", + "IsDone", + "IsReadOnly", + "IsMultiline", + "RendererHeightIsKnown", + "InEditingMode", + "InPasteMode", + "ViMode", + "ViNavigationMode", + "ViInsertMode", + "ViInsertMultipleMode", + "ViReplaceMode", + "ViSelectionMode", + "ViWaitingForTextObjectMode", + "ViDigraphMode", + "EmacsMode", + "EmacsInsertMode", + "EmacsSelectionMode", + "IsSearching", + "HasSearch", + "ControlIsSearchable", +] + +# Keep the original classnames for backwards compatibility. +HasValidationError = lambda: has_validation_error +HasArg = lambda: has_arg +IsDone = lambda: is_done +RendererHeightIsKnown = lambda: renderer_height_is_known +ViNavigationMode = lambda: vi_navigation_mode +InPasteMode = lambda: in_paste_mode +EmacsMode = lambda: emacs_mode +EmacsInsertMode = lambda: emacs_insert_mode +ViMode = lambda: vi_mode +IsSearching = lambda: is_searching +HasSearch = lambda: is_searching +ControlIsSearchable = lambda: control_is_searchable +EmacsSelectionMode = lambda: emacs_selection_mode +ViDigraphMode = lambda: vi_digraph_mode +ViWaitingForTextObjectMode = lambda: vi_waiting_for_text_object_mode +ViSelectionMode = lambda: vi_selection_mode +ViReplaceMode = lambda: vi_replace_mode +ViInsertMultipleMode = lambda: vi_insert_multiple_mode +ViInsertMode = lambda: vi_insert_mode +HasSelection = lambda: has_selection +HasCompletions = lambda: has_completions +IsReadOnly = lambda: is_read_only +IsMultiline = lambda: is_multiline + +HasFocus = has_focus # No lambda here! (Has_focus is callable that returns a callable.) +InEditingMode = in_editing_mode diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/filters/utils.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/filters/utils.py index 8aeb30def8..aaf44aa87e 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/filters/utils.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/filters/utils.py @@ -1,41 +1,41 @@ -from typing import Dict - -from .base import Always, Filter, FilterOrBool, Never - -__all__ = [ - "to_filter", - "is_true", -] - - -_always = Always() -_never = Never() - - -_bool_to_filter: Dict[bool, Filter] = { - True: _always, - False: _never, -} - - -def to_filter(bool_or_filter: FilterOrBool) -> Filter: - """ - Accept both booleans and Filters as input and - turn it into a Filter. - """ - if isinstance(bool_or_filter, bool): - return _bool_to_filter[bool_or_filter] - - if isinstance(bool_or_filter, Filter): - return bool_or_filter - - raise TypeError("Expecting a bool or a Filter instance. Got %r" % bool_or_filter) - - -def is_true(value: FilterOrBool) -> bool: - """ - Test whether `value` is True. In case of a Filter, call it. - - :param value: Boolean or `Filter` instance. - """ - return to_filter(value)() +from typing import Dict + +from .base import Always, Filter, FilterOrBool, Never + +__all__ = [ + "to_filter", + "is_true", +] + + +_always = Always() +_never = Never() + + +_bool_to_filter: Dict[bool, Filter] = { + True: _always, + False: _never, +} + + +def to_filter(bool_or_filter: FilterOrBool) -> Filter: + """ + Accept both booleans and Filters as input and + turn it into a Filter. + """ + if isinstance(bool_or_filter, bool): + return _bool_to_filter[bool_or_filter] + + if isinstance(bool_or_filter, Filter): + return bool_or_filter + + raise TypeError("Expecting a bool or a Filter instance. Got %r" % bool_or_filter) + + +def is_true(value: FilterOrBool) -> bool: + """ + Test whether `value` is True. In case of a Filter, call it. + + :param value: Boolean or `Filter` instance. + """ + return to_filter(value)() diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/formatted_text/__init__.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/formatted_text/__init__.py index 7272539fcc..f0c92c96f9 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/formatted_text/__init__.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/formatted_text/__init__.py @@ -1,54 +1,54 @@ -""" -Many places in prompt_toolkit can take either plain text, or formatted text. -For instance the :func:`~prompt_toolkit.shortcuts.prompt` function takes either -plain text or formatted text for the prompt. The -:class:`~prompt_toolkit.layout.FormattedTextControl` can also take either plain -text or formatted text. - -In any case, there is an input that can either be just plain text (a string), -an :class:`.HTML` object, an :class:`.ANSI` object or a sequence of -`(style_string, text)` tuples. The :func:`.to_formatted_text` conversion -function takes any of these and turns all of them into such a tuple sequence. -""" -from .ansi import ANSI -from .base import ( - AnyFormattedText, - FormattedText, - StyleAndTextTuples, - Template, - is_formatted_text, - merge_formatted_text, - to_formatted_text, -) -from .html import HTML -from .pygments import PygmentsTokens -from .utils import ( - fragment_list_len, - fragment_list_to_text, - fragment_list_width, - split_lines, +""" +Many places in prompt_toolkit can take either plain text, or formatted text. +For instance the :func:`~prompt_toolkit.shortcuts.prompt` function takes either +plain text or formatted text for the prompt. The +:class:`~prompt_toolkit.layout.FormattedTextControl` can also take either plain +text or formatted text. + +In any case, there is an input that can either be just plain text (a string), +an :class:`.HTML` object, an :class:`.ANSI` object or a sequence of +`(style_string, text)` tuples. The :func:`.to_formatted_text` conversion +function takes any of these and turns all of them into such a tuple sequence. +""" +from .ansi import ANSI +from .base import ( + AnyFormattedText, + FormattedText, + StyleAndTextTuples, + Template, + is_formatted_text, + merge_formatted_text, + to_formatted_text, +) +from .html import HTML +from .pygments import PygmentsTokens +from .utils import ( + fragment_list_len, + fragment_list_to_text, + fragment_list_width, + split_lines, to_plain_text, -) - -__all__ = [ - # Base. - "AnyFormattedText", - "to_formatted_text", - "is_formatted_text", - "Template", - "merge_formatted_text", - "FormattedText", - "StyleAndTextTuples", - # HTML. - "HTML", - # ANSI. - "ANSI", - # Pygments. - "PygmentsTokens", - # Utils. - "fragment_list_len", - "fragment_list_width", - "fragment_list_to_text", - "split_lines", +) + +__all__ = [ + # Base. + "AnyFormattedText", + "to_formatted_text", + "is_formatted_text", + "Template", + "merge_formatted_text", + "FormattedText", + "StyleAndTextTuples", + # HTML. + "HTML", + # ANSI. + "ANSI", + # Pygments. + "PygmentsTokens", + # Utils. + "fragment_list_len", + "fragment_list_width", + "fragment_list_to_text", + "split_lines", "to_plain_text", -] +] diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/formatted_text/ansi.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/formatted_text/ansi.py index 2d8c5dac95..3d57063357 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/formatted_text/ansi.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/formatted_text/ansi.py @@ -1,117 +1,117 @@ -from typing import Generator, List, Optional - -from prompt_toolkit.output.vt100 import BG_ANSI_COLORS, FG_ANSI_COLORS -from prompt_toolkit.output.vt100 import _256_colors as _256_colors_table - -from .base import StyleAndTextTuples - -__all__ = [ - "ANSI", - "ansi_escape", -] - - -class ANSI: - """ - ANSI formatted text. - Take something ANSI escaped text, for use as a formatted string. E.g. - - :: - - ANSI('\\x1b[31mhello \\x1b[32mworld') - - Characters between ``\\001`` and ``\\002`` are supposed to have a zero width - when printed, but these are literally sent to the terminal output. This can - be used for instance, for inserting Final Term prompt commands. They will - be translated into a prompt_toolkit '[ZeroWidthEscape]' fragment. - """ - - def __init__(self, value: str) -> None: - self.value = value - self._formatted_text: StyleAndTextTuples = [] - - # Default style attributes. - self._color: Optional[str] = None - self._bgcolor: Optional[str] = None - self._bold = False - self._underline = False - self._strike = False - self._italic = False - self._blink = False - self._reverse = False - self._hidden = False - - # Process received text. - parser = self._parse_corot() - parser.send(None) # type: ignore - for c in value: - parser.send(c) - - def _parse_corot(self) -> Generator[None, str, None]: - """ - Coroutine that parses the ANSI escape sequences. - """ - style = "" - formatted_text = self._formatted_text - - while True: +from typing import Generator, List, Optional + +from prompt_toolkit.output.vt100 import BG_ANSI_COLORS, FG_ANSI_COLORS +from prompt_toolkit.output.vt100 import _256_colors as _256_colors_table + +from .base import StyleAndTextTuples + +__all__ = [ + "ANSI", + "ansi_escape", +] + + +class ANSI: + """ + ANSI formatted text. + Take something ANSI escaped text, for use as a formatted string. E.g. + + :: + + ANSI('\\x1b[31mhello \\x1b[32mworld') + + Characters between ``\\001`` and ``\\002`` are supposed to have a zero width + when printed, but these are literally sent to the terminal output. This can + be used for instance, for inserting Final Term prompt commands. They will + be translated into a prompt_toolkit '[ZeroWidthEscape]' fragment. + """ + + def __init__(self, value: str) -> None: + self.value = value + self._formatted_text: StyleAndTextTuples = [] + + # Default style attributes. + self._color: Optional[str] = None + self._bgcolor: Optional[str] = None + self._bold = False + self._underline = False + self._strike = False + self._italic = False + self._blink = False + self._reverse = False + self._hidden = False + + # Process received text. + parser = self._parse_corot() + parser.send(None) # type: ignore + for c in value: + parser.send(c) + + def _parse_corot(self) -> Generator[None, str, None]: + """ + Coroutine that parses the ANSI escape sequences. + """ + style = "" + formatted_text = self._formatted_text + + while True: # NOTE: CSI is a special token within a stream of characters that # introduces an ANSI control sequence used to set the # style attributes of the following characters. - csi = False - - c = yield - - # Everything between \001 and \002 should become a ZeroWidthEscape. - if c == "\001": - escaped_text = "" - while c != "\002": - c = yield - if c == "\002": - formatted_text.append(("[ZeroWidthEscape]", escaped_text)) - c = yield - break - else: - escaped_text += c - + csi = False + + c = yield + + # Everything between \001 and \002 should become a ZeroWidthEscape. + if c == "\001": + escaped_text = "" + while c != "\002": + c = yield + if c == "\002": + formatted_text.append(("[ZeroWidthEscape]", escaped_text)) + c = yield + break + else: + escaped_text += c + # Check for CSI - if c == "\x1b": - # Start of color escape sequence. - square_bracket = yield - if square_bracket == "[": - csi = True - else: - continue - elif c == "\x9b": - csi = True - - if csi: - # Got a CSI sequence. Color codes are following. - current = "" - params = [] - - while True: - char = yield + if c == "\x1b": + # Start of color escape sequence. + square_bracket = yield + if square_bracket == "[": + csi = True + else: + continue + elif c == "\x9b": + csi = True + + if csi: + # Got a CSI sequence. Color codes are following. + current = "" + params = [] + + while True: + char = yield # Construct number - if char.isdigit(): - current += char + if char.isdigit(): + current += char # Eval number - else: + else: # Limit and save number value - params.append(min(int(current or 0), 9999)) + params.append(min(int(current or 0), 9999)) # Get delimiter token if present - if char == ";": - current = "" + if char == ";": + current = "" # Check and evaluate color codes - elif char == "m": - # Set attributes and token. - self._select_graphic_rendition(params) - style = self._create_style_string() - break + elif char == "m": + # Set attributes and token. + self._select_graphic_rendition(params) + style = self._create_style_string() + break # Check and evaluate cursor forward elif char == "C": @@ -120,163 +120,163 @@ class ANSI: formatted_text.append((style, " ")) break - else: - # Ignore unsupported sequence. - break - else: - # Add current character. - # NOTE: At this point, we could merge the current character - # into the previous tuple if the style did not change, - # however, it's not worth the effort given that it will - # be "Exploded" once again when it's rendered to the - # output. - formatted_text.append((style, c)) - - def _select_graphic_rendition(self, attrs: List[int]) -> None: - """ - Taken a list of graphics attributes and apply changes. - """ - if not attrs: - attrs = [0] - else: - attrs = list(attrs[::-1]) - - while attrs: - attr = attrs.pop() - - if attr in _fg_colors: - self._color = _fg_colors[attr] - elif attr in _bg_colors: - self._bgcolor = _bg_colors[attr] - elif attr == 1: - self._bold = True + else: + # Ignore unsupported sequence. + break + else: + # Add current character. + # NOTE: At this point, we could merge the current character + # into the previous tuple if the style did not change, + # however, it's not worth the effort given that it will + # be "Exploded" once again when it's rendered to the + # output. + formatted_text.append((style, c)) + + def _select_graphic_rendition(self, attrs: List[int]) -> None: + """ + Taken a list of graphics attributes and apply changes. + """ + if not attrs: + attrs = [0] + else: + attrs = list(attrs[::-1]) + + while attrs: + attr = attrs.pop() + + if attr in _fg_colors: + self._color = _fg_colors[attr] + elif attr in _bg_colors: + self._bgcolor = _bg_colors[attr] + elif attr == 1: + self._bold = True # elif attr == 2: # self._faint = True - elif attr == 3: - self._italic = True - elif attr == 4: - self._underline = True - elif attr == 5: + elif attr == 3: + self._italic = True + elif attr == 4: + self._underline = True + elif attr == 5: self._blink = True # Slow blink - elif attr == 6: + elif attr == 6: self._blink = True # Fast blink - elif attr == 7: - self._reverse = True - elif attr == 8: - self._hidden = True - elif attr == 9: - self._strike = True - elif attr == 22: + elif attr == 7: + self._reverse = True + elif attr == 8: + self._hidden = True + elif attr == 9: + self._strike = True + elif attr == 22: self._bold = False # Normal intensity - elif attr == 23: - self._italic = False - elif attr == 24: - self._underline = False - elif attr == 25: - self._blink = False - elif attr == 27: - self._reverse = False + elif attr == 23: + self._italic = False + elif attr == 24: + self._underline = False + elif attr == 25: + self._blink = False + elif attr == 27: + self._reverse = False elif attr == 28: self._hidden = False - elif attr == 29: - self._strike = False - elif not attr: + elif attr == 29: + self._strike = False + elif not attr: # Reset all style attributes - self._color = None - self._bgcolor = None - self._bold = False - self._underline = False - self._strike = False - self._italic = False - self._blink = False - self._reverse = False - self._hidden = False - - elif attr in (38, 48) and len(attrs) > 1: - n = attrs.pop() - - # 256 colors. - if n == 5 and len(attrs) >= 1: - if attr == 38: - m = attrs.pop() - self._color = _256_colors.get(m) - elif attr == 48: - m = attrs.pop() - self._bgcolor = _256_colors.get(m) - - # True colors. - if n == 2 and len(attrs) >= 3: - try: - color_str = "#%02x%02x%02x" % ( - attrs.pop(), - attrs.pop(), - attrs.pop(), - ) - except IndexError: - pass - else: - if attr == 38: - self._color = color_str - elif attr == 48: - self._bgcolor = color_str - - def _create_style_string(self) -> str: - """ - Turn current style flags into a string for usage in a formatted text. - """ - result = [] - if self._color: - result.append(self._color) - if self._bgcolor: - result.append("bg:" + self._bgcolor) - if self._bold: - result.append("bold") - if self._underline: - result.append("underline") - if self._strike: - result.append("strike") - if self._italic: - result.append("italic") - if self._blink: - result.append("blink") - if self._reverse: - result.append("reverse") - if self._hidden: - result.append("hidden") - - return " ".join(result) - - def __repr__(self) -> str: - return "ANSI(%r)" % (self.value,) - - def __pt_formatted_text__(self) -> StyleAndTextTuples: - return self._formatted_text - - def format(self, *args: str, **kwargs: str) -> "ANSI": - """ - Like `str.format`, but make sure that the arguments are properly - escaped. (No ANSI escapes can be injected.) - """ - # Escape all the arguments. - args = tuple(ansi_escape(a) for a in args) - kwargs = {k: ansi_escape(v) for k, v in kwargs.items()} - - return ANSI(self.value.format(*args, **kwargs)) - - -# Mapping of the ANSI color codes to their names. -_fg_colors = {v: k for k, v in FG_ANSI_COLORS.items()} -_bg_colors = {v: k for k, v in BG_ANSI_COLORS.items()} - -# Mapping of the escape codes for 256colors to their 'ffffff' value. -_256_colors = {} - -for i, (r, g, b) in enumerate(_256_colors_table.colors): - _256_colors[i] = "#%02x%02x%02x" % (r, g, b) - - -def ansi_escape(text: str) -> str: - """ - Replace characters with a special meaning. - """ - return text.replace("\x1b", "?").replace("\b", "?") + self._color = None + self._bgcolor = None + self._bold = False + self._underline = False + self._strike = False + self._italic = False + self._blink = False + self._reverse = False + self._hidden = False + + elif attr in (38, 48) and len(attrs) > 1: + n = attrs.pop() + + # 256 colors. + if n == 5 and len(attrs) >= 1: + if attr == 38: + m = attrs.pop() + self._color = _256_colors.get(m) + elif attr == 48: + m = attrs.pop() + self._bgcolor = _256_colors.get(m) + + # True colors. + if n == 2 and len(attrs) >= 3: + try: + color_str = "#%02x%02x%02x" % ( + attrs.pop(), + attrs.pop(), + attrs.pop(), + ) + except IndexError: + pass + else: + if attr == 38: + self._color = color_str + elif attr == 48: + self._bgcolor = color_str + + def _create_style_string(self) -> str: + """ + Turn current style flags into a string for usage in a formatted text. + """ + result = [] + if self._color: + result.append(self._color) + if self._bgcolor: + result.append("bg:" + self._bgcolor) + if self._bold: + result.append("bold") + if self._underline: + result.append("underline") + if self._strike: + result.append("strike") + if self._italic: + result.append("italic") + if self._blink: + result.append("blink") + if self._reverse: + result.append("reverse") + if self._hidden: + result.append("hidden") + + return " ".join(result) + + def __repr__(self) -> str: + return "ANSI(%r)" % (self.value,) + + def __pt_formatted_text__(self) -> StyleAndTextTuples: + return self._formatted_text + + def format(self, *args: str, **kwargs: str) -> "ANSI": + """ + Like `str.format`, but make sure that the arguments are properly + escaped. (No ANSI escapes can be injected.) + """ + # Escape all the arguments. + args = tuple(ansi_escape(a) for a in args) + kwargs = {k: ansi_escape(v) for k, v in kwargs.items()} + + return ANSI(self.value.format(*args, **kwargs)) + + +# Mapping of the ANSI color codes to their names. +_fg_colors = {v: k for k, v in FG_ANSI_COLORS.items()} +_bg_colors = {v: k for k, v in BG_ANSI_COLORS.items()} + +# Mapping of the escape codes for 256colors to their 'ffffff' value. +_256_colors = {} + +for i, (r, g, b) in enumerate(_256_colors_table.colors): + _256_colors[i] = "#%02x%02x%02x" % (r, g, b) + + +def ansi_escape(text: str) -> str: + """ + Replace characters with a special meaning. + """ + return text.replace("\x1b", "?").replace("\b", "?") diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/formatted_text/base.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/formatted_text/base.py index 95b6cf716a..c1761f2640 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/formatted_text/base.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/formatted_text/base.py @@ -1,176 +1,176 @@ -from typing import TYPE_CHECKING, Any, Callable, Iterable, List, Tuple, Union, cast - -from prompt_toolkit.mouse_events import MouseEvent - -if TYPE_CHECKING: - from typing_extensions import Protocol - -__all__ = [ - "OneStyleAndTextTuple", - "StyleAndTextTuples", - "MagicFormattedText", - "AnyFormattedText", - "to_formatted_text", - "is_formatted_text", - "Template", - "merge_formatted_text", - "FormattedText", -] - -OneStyleAndTextTuple = Union[ - Tuple[str, str], Tuple[str, str, Callable[[MouseEvent], None]] -] - -# List of (style, text) tuples. -StyleAndTextTuples = List[OneStyleAndTextTuple] - - -if TYPE_CHECKING: - from typing_extensions import TypeGuard - - class MagicFormattedText(Protocol): - """ - Any object that implements ``__pt_formatted_text__`` represents formatted - text. - """ - - def __pt_formatted_text__(self) -> StyleAndTextTuples: - ... - - -AnyFormattedText = Union[ - str, - "MagicFormattedText", - StyleAndTextTuples, - # Callable[[], 'AnyFormattedText'] # Recursive definition not supported by mypy. - Callable[[], Any], - None, -] - - -def to_formatted_text( - value: AnyFormattedText, style: str = "", auto_convert: bool = False -) -> "FormattedText": - """ - Convert the given value (which can be formatted text) into a list of text - fragments. (Which is the canonical form of formatted text.) The outcome is - always a `FormattedText` instance, which is a list of (style, text) tuples. - - It can take a plain text string, an `HTML` or `ANSI` object, anything that - implements `__pt_formatted_text__` or a callable that takes no arguments and - returns one of those. - - :param style: An additional style string which is applied to all text - fragments. - :param auto_convert: If `True`, also accept other types, and convert them - to a string first. - """ - result: Union[FormattedText, StyleAndTextTuples] - - if value is None: - result = [] - elif isinstance(value, str): - result = [("", value)] - elif isinstance(value, list): - result = value # StyleAndTextTuples - elif hasattr(value, "__pt_formatted_text__"): - result = cast("MagicFormattedText", value).__pt_formatted_text__() - elif callable(value): - return to_formatted_text(value(), style=style) - elif auto_convert: - result = [("", "{}".format(value))] - else: - raise ValueError( - "No formatted text. Expecting a unicode object, " - "HTML, ANSI or a FormattedText instance. Got %r" % (value,) - ) - - # Apply extra style. - if style: - result = cast( - StyleAndTextTuples, - [(style + " " + item_style, *rest) for item_style, *rest in result], - ) - - # Make sure the result is wrapped in a `FormattedText`. Among other - # reasons, this is important for `print_formatted_text` to work correctly - # and distinguish between lists and formatted text. - if isinstance(result, FormattedText): - return result - else: - return FormattedText(result) - - -def is_formatted_text(value: object) -> "TypeGuard[AnyFormattedText]": - """ - Check whether the input is valid formatted text (for use in assert - statements). - In case of a callable, it doesn't check the return type. - """ - if callable(value): - return True - if isinstance(value, (str, list)): - return True - if hasattr(value, "__pt_formatted_text__"): - return True - return False - - -class FormattedText(StyleAndTextTuples): - """ - A list of ``(style, text)`` tuples. - - (In some situations, this can also be ``(style, text, mouse_handler)`` - tuples.) - """ - - def __pt_formatted_text__(self) -> StyleAndTextTuples: - return self - - def __repr__(self) -> str: - return "FormattedText(%s)" % super().__repr__() - - -class Template: - """ - Template for string interpolation with formatted text. - - Example:: - - Template(' ... {} ... ').format(HTML(...)) - - :param text: Plain text. - """ - - def __init__(self, text: str) -> None: - assert "{0}" not in text - self.text = text - - def format(self, *values: AnyFormattedText) -> AnyFormattedText: - def get_result() -> AnyFormattedText: - # Split the template in parts. - parts = self.text.split("{}") - assert len(parts) - 1 == len(values) - - result = FormattedText() - for part, val in zip(parts, values): - result.append(("", part)) - result.extend(to_formatted_text(val)) - result.append(("", parts[-1])) - return result - - return get_result - - -def merge_formatted_text(items: Iterable[AnyFormattedText]) -> AnyFormattedText: - """ - Merge (Concatenate) several pieces of formatted text together. - """ - - def _merge_formatted_text() -> AnyFormattedText: - result = FormattedText() - for i in items: - result.extend(to_formatted_text(i)) - return result - - return _merge_formatted_text +from typing import TYPE_CHECKING, Any, Callable, Iterable, List, Tuple, Union, cast + +from prompt_toolkit.mouse_events import MouseEvent + +if TYPE_CHECKING: + from typing_extensions import Protocol + +__all__ = [ + "OneStyleAndTextTuple", + "StyleAndTextTuples", + "MagicFormattedText", + "AnyFormattedText", + "to_formatted_text", + "is_formatted_text", + "Template", + "merge_formatted_text", + "FormattedText", +] + +OneStyleAndTextTuple = Union[ + Tuple[str, str], Tuple[str, str, Callable[[MouseEvent], None]] +] + +# List of (style, text) tuples. +StyleAndTextTuples = List[OneStyleAndTextTuple] + + +if TYPE_CHECKING: + from typing_extensions import TypeGuard + + class MagicFormattedText(Protocol): + """ + Any object that implements ``__pt_formatted_text__`` represents formatted + text. + """ + + def __pt_formatted_text__(self) -> StyleAndTextTuples: + ... + + +AnyFormattedText = Union[ + str, + "MagicFormattedText", + StyleAndTextTuples, + # Callable[[], 'AnyFormattedText'] # Recursive definition not supported by mypy. + Callable[[], Any], + None, +] + + +def to_formatted_text( + value: AnyFormattedText, style: str = "", auto_convert: bool = False +) -> "FormattedText": + """ + Convert the given value (which can be formatted text) into a list of text + fragments. (Which is the canonical form of formatted text.) The outcome is + always a `FormattedText` instance, which is a list of (style, text) tuples. + + It can take a plain text string, an `HTML` or `ANSI` object, anything that + implements `__pt_formatted_text__` or a callable that takes no arguments and + returns one of those. + + :param style: An additional style string which is applied to all text + fragments. + :param auto_convert: If `True`, also accept other types, and convert them + to a string first. + """ + result: Union[FormattedText, StyleAndTextTuples] + + if value is None: + result = [] + elif isinstance(value, str): + result = [("", value)] + elif isinstance(value, list): + result = value # StyleAndTextTuples + elif hasattr(value, "__pt_formatted_text__"): + result = cast("MagicFormattedText", value).__pt_formatted_text__() + elif callable(value): + return to_formatted_text(value(), style=style) + elif auto_convert: + result = [("", "{}".format(value))] + else: + raise ValueError( + "No formatted text. Expecting a unicode object, " + "HTML, ANSI or a FormattedText instance. Got %r" % (value,) + ) + + # Apply extra style. + if style: + result = cast( + StyleAndTextTuples, + [(style + " " + item_style, *rest) for item_style, *rest in result], + ) + + # Make sure the result is wrapped in a `FormattedText`. Among other + # reasons, this is important for `print_formatted_text` to work correctly + # and distinguish between lists and formatted text. + if isinstance(result, FormattedText): + return result + else: + return FormattedText(result) + + +def is_formatted_text(value: object) -> "TypeGuard[AnyFormattedText]": + """ + Check whether the input is valid formatted text (for use in assert + statements). + In case of a callable, it doesn't check the return type. + """ + if callable(value): + return True + if isinstance(value, (str, list)): + return True + if hasattr(value, "__pt_formatted_text__"): + return True + return False + + +class FormattedText(StyleAndTextTuples): + """ + A list of ``(style, text)`` tuples. + + (In some situations, this can also be ``(style, text, mouse_handler)`` + tuples.) + """ + + def __pt_formatted_text__(self) -> StyleAndTextTuples: + return self + + def __repr__(self) -> str: + return "FormattedText(%s)" % super().__repr__() + + +class Template: + """ + Template for string interpolation with formatted text. + + Example:: + + Template(' ... {} ... ').format(HTML(...)) + + :param text: Plain text. + """ + + def __init__(self, text: str) -> None: + assert "{0}" not in text + self.text = text + + def format(self, *values: AnyFormattedText) -> AnyFormattedText: + def get_result() -> AnyFormattedText: + # Split the template in parts. + parts = self.text.split("{}") + assert len(parts) - 1 == len(values) + + result = FormattedText() + for part, val in zip(parts, values): + result.append(("", part)) + result.extend(to_formatted_text(val)) + result.append(("", parts[-1])) + return result + + return get_result + + +def merge_formatted_text(items: Iterable[AnyFormattedText]) -> AnyFormattedText: + """ + Merge (Concatenate) several pieces of formatted text together. + """ + + def _merge_formatted_text() -> AnyFormattedText: + result = FormattedText() + for i in items: + result.extend(to_formatted_text(i)) + return result + + return _merge_formatted_text diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/formatted_text/html.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/formatted_text/html.py index 58d4c08c82..06c6020f54 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/formatted_text/html.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/formatted_text/html.py @@ -1,138 +1,138 @@ -import xml.dom.minidom as minidom -from typing import Any, List, Tuple, Union - -from .base import FormattedText, StyleAndTextTuples - -__all__ = ["HTML"] - - -class HTML: - """ - HTML formatted text. - Take something HTML-like, for use as a formatted string. - - :: - - # Turn something into red. - HTML('<style fg="ansired" bg="#00ff44">...</style>') - - # Italic, bold, underline and strike. - HTML('<i>...</i>') - HTML('<b>...</b>') - HTML('<u>...</u>') - HTML('<s>...</s>') - - All HTML elements become available as a "class" in the style sheet. - E.g. ``<username>...</username>`` can be styled, by setting a style for - ``username``. - """ - - def __init__(self, value: str) -> None: - self.value = value - document = minidom.parseString("<html-root>%s</html-root>" % (value,)) - - result: StyleAndTextTuples = [] - name_stack: List[str] = [] - fg_stack: List[str] = [] - bg_stack: List[str] = [] - - def get_current_style() -> str: - "Build style string for current node." - parts = [] - if name_stack: - parts.append("class:" + ",".join(name_stack)) - - if fg_stack: - parts.append("fg:" + fg_stack[-1]) - if bg_stack: - parts.append("bg:" + bg_stack[-1]) - return " ".join(parts) - - def process_node(node: Any) -> None: - "Process node recursively." - for child in node.childNodes: - if child.nodeType == child.TEXT_NODE: - result.append((get_current_style(), child.data)) - else: - add_to_name_stack = child.nodeName not in ( - "#document", - "html-root", - "style", - ) - fg = bg = "" - - for k, v in child.attributes.items(): - if k == "fg": - fg = v - if k == "bg": - bg = v - if k == "color": - fg = v # Alias for 'fg'. - - # Check for spaces in attributes. This would result in - # invalid style strings otherwise. - if " " in fg: - raise ValueError('"fg" attribute contains a space.') - if " " in bg: - raise ValueError('"bg" attribute contains a space.') - - if add_to_name_stack: - name_stack.append(child.nodeName) - if fg: - fg_stack.append(fg) - if bg: - bg_stack.append(bg) - - process_node(child) - - if add_to_name_stack: - name_stack.pop() - if fg: - fg_stack.pop() - if bg: - bg_stack.pop() - - process_node(document) - - self.formatted_text = FormattedText(result) - - def __repr__(self) -> str: - return "HTML(%r)" % (self.value,) - - def __pt_formatted_text__(self) -> StyleAndTextTuples: - return self.formatted_text - - def format(self, *args: object, **kwargs: object) -> "HTML": - """ - Like `str.format`, but make sure that the arguments are properly - escaped. - """ - # Escape all the arguments. - escaped_args = [html_escape(a) for a in args] - escaped_kwargs = {k: html_escape(v) for k, v in kwargs.items()} - - return HTML(self.value.format(*escaped_args, **escaped_kwargs)) - - def __mod__(self, value: Union[object, Tuple[object, ...]]) -> "HTML": - """ - HTML('<b>%s</b>') % value - """ - if not isinstance(value, tuple): - value = (value,) - - value = tuple(html_escape(i) for i in value) - return HTML(self.value % value) - - -def html_escape(text: object) -> str: - # The string interpolation functions also take integers and other types. - # Convert to string first. - if not isinstance(text, str): - text = "{}".format(text) - - return ( - text.replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace('"', """) - ) +import xml.dom.minidom as minidom +from typing import Any, List, Tuple, Union + +from .base import FormattedText, StyleAndTextTuples + +__all__ = ["HTML"] + + +class HTML: + """ + HTML formatted text. + Take something HTML-like, for use as a formatted string. + + :: + + # Turn something into red. + HTML('<style fg="ansired" bg="#00ff44">...</style>') + + # Italic, bold, underline and strike. + HTML('<i>...</i>') + HTML('<b>...</b>') + HTML('<u>...</u>') + HTML('<s>...</s>') + + All HTML elements become available as a "class" in the style sheet. + E.g. ``<username>...</username>`` can be styled, by setting a style for + ``username``. + """ + + def __init__(self, value: str) -> None: + self.value = value + document = minidom.parseString("<html-root>%s</html-root>" % (value,)) + + result: StyleAndTextTuples = [] + name_stack: List[str] = [] + fg_stack: List[str] = [] + bg_stack: List[str] = [] + + def get_current_style() -> str: + "Build style string for current node." + parts = [] + if name_stack: + parts.append("class:" + ",".join(name_stack)) + + if fg_stack: + parts.append("fg:" + fg_stack[-1]) + if bg_stack: + parts.append("bg:" + bg_stack[-1]) + return " ".join(parts) + + def process_node(node: Any) -> None: + "Process node recursively." + for child in node.childNodes: + if child.nodeType == child.TEXT_NODE: + result.append((get_current_style(), child.data)) + else: + add_to_name_stack = child.nodeName not in ( + "#document", + "html-root", + "style", + ) + fg = bg = "" + + for k, v in child.attributes.items(): + if k == "fg": + fg = v + if k == "bg": + bg = v + if k == "color": + fg = v # Alias for 'fg'. + + # Check for spaces in attributes. This would result in + # invalid style strings otherwise. + if " " in fg: + raise ValueError('"fg" attribute contains a space.') + if " " in bg: + raise ValueError('"bg" attribute contains a space.') + + if add_to_name_stack: + name_stack.append(child.nodeName) + if fg: + fg_stack.append(fg) + if bg: + bg_stack.append(bg) + + process_node(child) + + if add_to_name_stack: + name_stack.pop() + if fg: + fg_stack.pop() + if bg: + bg_stack.pop() + + process_node(document) + + self.formatted_text = FormattedText(result) + + def __repr__(self) -> str: + return "HTML(%r)" % (self.value,) + + def __pt_formatted_text__(self) -> StyleAndTextTuples: + return self.formatted_text + + def format(self, *args: object, **kwargs: object) -> "HTML": + """ + Like `str.format`, but make sure that the arguments are properly + escaped. + """ + # Escape all the arguments. + escaped_args = [html_escape(a) for a in args] + escaped_kwargs = {k: html_escape(v) for k, v in kwargs.items()} + + return HTML(self.value.format(*escaped_args, **escaped_kwargs)) + + def __mod__(self, value: Union[object, Tuple[object, ...]]) -> "HTML": + """ + HTML('<b>%s</b>') % value + """ + if not isinstance(value, tuple): + value = (value,) + + value = tuple(html_escape(i) for i in value) + return HTML(self.value % value) + + +def html_escape(text: object) -> str: + # The string interpolation functions also take integers and other types. + # Convert to string first. + if not isinstance(text, str): + text = "{}".format(text) + + return ( + text.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace('"', """) + ) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/formatted_text/pygments.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/formatted_text/pygments.py index 34197840e5..dd16f0efbe 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/formatted_text/pygments.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/formatted_text/pygments.py @@ -1,30 +1,30 @@ -from typing import TYPE_CHECKING, List, Tuple - -from prompt_toolkit.styles.pygments import pygments_token_to_classname - -from .base import StyleAndTextTuples - -if TYPE_CHECKING: - from pygments.token import Token - -__all__ = [ - "PygmentsTokens", -] - - -class PygmentsTokens: - """ - Turn a pygments token list into a list of prompt_toolkit text fragments - (``(style_str, text)`` tuples). - """ - - def __init__(self, token_list: List[Tuple["Token", str]]) -> None: - self.token_list = token_list - - def __pt_formatted_text__(self) -> StyleAndTextTuples: - result: StyleAndTextTuples = [] - - for token, text in self.token_list: - result.append(("class:" + pygments_token_to_classname(token), text)) - - return result +from typing import TYPE_CHECKING, List, Tuple + +from prompt_toolkit.styles.pygments import pygments_token_to_classname + +from .base import StyleAndTextTuples + +if TYPE_CHECKING: + from pygments.token import Token + +__all__ = [ + "PygmentsTokens", +] + + +class PygmentsTokens: + """ + Turn a pygments token list into a list of prompt_toolkit text fragments + (``(style_str, text)`` tuples). + """ + + def __init__(self, token_list: List[Tuple["Token", str]]) -> None: + self.token_list = token_list + + def __pt_formatted_text__(self) -> StyleAndTextTuples: + result: StyleAndTextTuples = [] + + for token, text in self.token_list: + result.append(("class:" + pygments_token_to_classname(token), text)) + + return result diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/formatted_text/utils.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/formatted_text/utils.py index 126a6e0b39..cda4233e06 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/formatted_text/utils.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/formatted_text/utils.py @@ -1,29 +1,29 @@ -""" -Utilities for manipulating formatted text. - -When ``to_formatted_text`` has been called, we get a list of ``(style, text)`` -tuples. This file contains functions for manipulating such a list. -""" -from typing import Iterable, cast - -from prompt_toolkit.utils import get_cwidth - +""" +Utilities for manipulating formatted text. + +When ``to_formatted_text`` has been called, we get a list of ``(style, text)`` +tuples. This file contains functions for manipulating such a list. +""" +from typing import Iterable, cast + +from prompt_toolkit.utils import get_cwidth + from .base import ( AnyFormattedText, OneStyleAndTextTuple, StyleAndTextTuples, to_formatted_text, ) - -__all__ = [ + +__all__ = [ "to_plain_text", - "fragment_list_len", - "fragment_list_width", - "fragment_list_to_text", - "split_lines", -] - - + "fragment_list_len", + "fragment_list_width", + "fragment_list_to_text", + "split_lines", +] + + def to_plain_text(value: AnyFormattedText) -> str: """ Turn any kind of formatted text back into plain text. @@ -31,68 +31,68 @@ def to_plain_text(value: AnyFormattedText) -> str: return fragment_list_to_text(to_formatted_text(value)) -def fragment_list_len(fragments: StyleAndTextTuples) -> int: - """ - Return the amount of characters in this text fragment list. - - :param fragments: List of ``(style_str, text)`` or - ``(style_str, text, mouse_handler)`` tuples. - """ - ZeroWidthEscape = "[ZeroWidthEscape]" - return sum(len(item[1]) for item in fragments if ZeroWidthEscape not in item[0]) - - -def fragment_list_width(fragments: StyleAndTextTuples) -> int: - """ - Return the character width of this text fragment list. - (Take double width characters into account.) - - :param fragments: List of ``(style_str, text)`` or - ``(style_str, text, mouse_handler)`` tuples. - """ - ZeroWidthEscape = "[ZeroWidthEscape]" - return sum( - get_cwidth(c) - for item in fragments - for c in item[1] - if ZeroWidthEscape not in item[0] - ) - - -def fragment_list_to_text(fragments: StyleAndTextTuples) -> str: - """ - Concatenate all the text parts again. - - :param fragments: List of ``(style_str, text)`` or - ``(style_str, text, mouse_handler)`` tuples. - """ - ZeroWidthEscape = "[ZeroWidthEscape]" - return "".join(item[1] for item in fragments if ZeroWidthEscape not in item[0]) - - -def split_lines(fragments: StyleAndTextTuples) -> Iterable[StyleAndTextTuples]: - """ - Take a single list of (style_str, text) tuples and yield one such list for each - line. Just like str.split, this will yield at least one item. - - :param fragments: List of (style_str, text) or (style_str, text, mouse_handler) - tuples. - """ - line: StyleAndTextTuples = [] - - for style, string, *mouse_handler in fragments: - parts = string.split("\n") - - for part in parts[:-1]: - if part: - line.append(cast(OneStyleAndTextTuple, (style, part, *mouse_handler))) - yield line - line = [] - - line.append(cast(OneStyleAndTextTuple, (style, parts[-1], *mouse_handler))) - - # Always yield the last line, even when this is an empty line. This ensures - # that when `fragments` ends with a newline character, an additional empty - # line is yielded. (Otherwise, there's no way to differentiate between the - # cases where `fragments` does and doesn't end with a newline.) - yield line +def fragment_list_len(fragments: StyleAndTextTuples) -> int: + """ + Return the amount of characters in this text fragment list. + + :param fragments: List of ``(style_str, text)`` or + ``(style_str, text, mouse_handler)`` tuples. + """ + ZeroWidthEscape = "[ZeroWidthEscape]" + return sum(len(item[1]) for item in fragments if ZeroWidthEscape not in item[0]) + + +def fragment_list_width(fragments: StyleAndTextTuples) -> int: + """ + Return the character width of this text fragment list. + (Take double width characters into account.) + + :param fragments: List of ``(style_str, text)`` or + ``(style_str, text, mouse_handler)`` tuples. + """ + ZeroWidthEscape = "[ZeroWidthEscape]" + return sum( + get_cwidth(c) + for item in fragments + for c in item[1] + if ZeroWidthEscape not in item[0] + ) + + +def fragment_list_to_text(fragments: StyleAndTextTuples) -> str: + """ + Concatenate all the text parts again. + + :param fragments: List of ``(style_str, text)`` or + ``(style_str, text, mouse_handler)`` tuples. + """ + ZeroWidthEscape = "[ZeroWidthEscape]" + return "".join(item[1] for item in fragments if ZeroWidthEscape not in item[0]) + + +def split_lines(fragments: StyleAndTextTuples) -> Iterable[StyleAndTextTuples]: + """ + Take a single list of (style_str, text) tuples and yield one such list for each + line. Just like str.split, this will yield at least one item. + + :param fragments: List of (style_str, text) or (style_str, text, mouse_handler) + tuples. + """ + line: StyleAndTextTuples = [] + + for style, string, *mouse_handler in fragments: + parts = string.split("\n") + + for part in parts[:-1]: + if part: + line.append(cast(OneStyleAndTextTuple, (style, part, *mouse_handler))) + yield line + line = [] + + line.append(cast(OneStyleAndTextTuple, (style, parts[-1], *mouse_handler))) + + # Always yield the last line, even when this is an empty line. This ensures + # that when `fragments` ends with a newline character, an additional empty + # line is yielded. (Otherwise, there's no way to differentiate between the + # cases where `fragments` does and doesn't end with a newline.) + yield line diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/history.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/history.py index 345284a9ff..d6bc3e6779 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/history.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/history.py @@ -1,302 +1,302 @@ -""" -Implementations for the history of a `Buffer`. - -NOTE: There is no `DynamicHistory`: - This doesn't work well, because the `Buffer` needs to be able to attach - an event handler to the event when a history entry is loaded. This - loading can be done asynchronously and making the history swappable would - probably break this. -""" -import datetime -import os -import threading -from abc import ABCMeta, abstractmethod -from typing import AsyncGenerator, Iterable, List, Optional, Sequence, Tuple - -from prompt_toolkit.eventloop import get_event_loop - -__all__ = [ - "History", - "ThreadedHistory", - "DummyHistory", - "FileHistory", - "InMemoryHistory", -] - - -class History(metaclass=ABCMeta): - """ - Base ``History`` class. - - This also includes abstract methods for loading/storing history. - """ - - def __init__(self) -> None: - # In memory storage for strings. - self._loaded = False - - # History that's loaded already, in reverse order. Latest, most recent - # item first. - self._loaded_strings: List[str] = [] - - # - # Methods expected by `Buffer`. - # - - async def load(self) -> AsyncGenerator[str, None]: - """ - Load the history and yield all the entries in reverse order (latest, - most recent history entry first). - - This method can be called multiple times from the `Buffer` to - repopulate the history when prompting for a new input. So we are - responsible here for both caching, and making sure that strings that - were were appended to the history will be incorporated next time this - method is called. - """ - if not self._loaded: - self._loaded_strings = list(self.load_history_strings()) - self._loaded = True - - for item in self._loaded_strings: - yield item - - def get_strings(self) -> List[str]: - """ - Get the strings from the history that are loaded so far. - (In order. Oldest item first.) - """ - return self._loaded_strings[::-1] - - def append_string(self, string: str) -> None: - "Add string to the history." - self._loaded_strings.insert(0, string) - self.store_string(string) - - # - # Implementation for specific backends. - # - - @abstractmethod - def load_history_strings(self) -> Iterable[str]: - """ - This should be a generator that yields `str` instances. - - It should yield the most recent items first, because they are the most - important. (The history can already be used, even when it's only - partially loaded.) - """ - while False: - yield - - @abstractmethod - def store_string(self, string: str) -> None: - """ - Store the string in persistent storage. - """ - - -class ThreadedHistory(History): - """ - Wrapper around `History` implementations that run the `load()` generator in - a thread. - - Use this to increase the start-up time of prompt_toolkit applications. - History entries are available as soon as they are loaded. We don't have to - wait for everything to be loaded. - """ - - def __init__(self, history: History) -> None: - super().__init__() - - self.history = history - - self._load_thread: Optional[threading.Thread] = None - - # Lock for accessing/manipulating `_loaded_strings` and `_loaded` - # together in a consistent state. - self._lock = threading.Lock() - - # Events created by each `load()` call. Used to wait for new history - # entries from the loader thread. - self._string_load_events: List[threading.Event] = [] - - async def load(self) -> AsyncGenerator[str, None]: - """ - Like `History.load(), but call `self.load_history_strings()` in a - background thread. - """ - # Start the load thread, if this is called for the first time. - if not self._load_thread: - self._load_thread = threading.Thread( - target=self._in_load_thread, - daemon=True, - ) - self._load_thread.start() - - # Consume the `_loaded_strings` list, using asyncio. - loop = get_event_loop() - - # Create threading Event so that we can wait for new items. - event = threading.Event() - event.set() - self._string_load_events.append(event) - - items_yielded = 0 - - try: - while True: - # Wait for new items to be available. - # (Use a timeout, because the executor thread is not a daemon - # thread. The "slow-history.py" example would otherwise hang if - # Control-C is pressed before the history is fully loaded, - # because there's still this non-daemon executor thread waiting - # for this event.) - got_timeout = await loop.run_in_executor( - None, lambda: event.wait(timeout=0.5) - ) - if not got_timeout: - continue - - # Read new items (in lock). - def in_executor() -> Tuple[List[str], bool]: - with self._lock: - new_items = self._loaded_strings[items_yielded:] - done = self._loaded - event.clear() - return new_items, done - - new_items, done = await loop.run_in_executor(None, in_executor) - - items_yielded += len(new_items) - - for item in new_items: - yield item - - if done: - break - finally: - self._string_load_events.remove(event) - - def _in_load_thread(self) -> None: - try: - # Start with an empty list. In case `append_string()` was called - # before `load()` happened. Then `.store_string()` will have - # written these entries back to disk and we will reload it. - self._loaded_strings = [] - - for item in self.history.load_history_strings(): - with self._lock: - self._loaded_strings.append(item) - - for event in self._string_load_events: - event.set() - finally: - with self._lock: - self._loaded = True - for event in self._string_load_events: - event.set() - - def append_string(self, string: str) -> None: - with self._lock: - self._loaded_strings.insert(0, string) - self.store_string(string) - - # All of the following are proxied to `self.history`. - - def load_history_strings(self) -> Iterable[str]: - return self.history.load_history_strings() - - def store_string(self, string: str) -> None: - self.history.store_string(string) - - def __repr__(self) -> str: - return "ThreadedHistory(%r)" % (self.history,) - - -class InMemoryHistory(History): - """ - :class:`.History` class that keeps a list of all strings in memory. - - In order to prepopulate the history, it's possible to call either - `append_string` for all items or pass a list of strings to `__init__` here. - """ - - def __init__(self, history_strings: Optional[Sequence[str]] = None) -> None: - super().__init__() - # Emulating disk storage. - if history_strings is None: - self._storage = [] - else: - self._storage = list(history_strings) - - def load_history_strings(self) -> Iterable[str]: - for item in self._storage[::-1]: - yield item - - def store_string(self, string: str) -> None: - self._storage.append(string) - - -class DummyHistory(History): - """ - :class:`.History` object that doesn't remember anything. - """ - - def load_history_strings(self) -> Iterable[str]: - return [] - - def store_string(self, string: str) -> None: - pass - - def append_string(self, string: str) -> None: - # Don't remember this. - pass - - -class FileHistory(History): - """ - :class:`.History` class that stores all strings in a file. - """ - - def __init__(self, filename: str) -> None: - self.filename = filename - super(FileHistory, self).__init__() - - def load_history_strings(self) -> Iterable[str]: - strings: List[str] = [] - lines: List[str] = [] - - def add() -> None: - if lines: - # Join and drop trailing newline. - string = "".join(lines)[:-1] - - strings.append(string) - - if os.path.exists(self.filename): - with open(self.filename, "rb") as f: - for line_bytes in f: - line = line_bytes.decode("utf-8", errors="replace") - - if line.startswith("+"): - lines.append(line[1:]) - else: - add() - lines = [] - - add() - - # Reverse the order, because newest items have to go first. - return reversed(strings) - - def store_string(self, string: str) -> None: - # Save to file. - with open(self.filename, "ab") as f: - - def write(t: str) -> None: - f.write(t.encode("utf-8")) - - write("\n# %s\n" % datetime.datetime.now()) - for line in string.split("\n"): - write("+%s\n" % line) +""" +Implementations for the history of a `Buffer`. + +NOTE: There is no `DynamicHistory`: + This doesn't work well, because the `Buffer` needs to be able to attach + an event handler to the event when a history entry is loaded. This + loading can be done asynchronously and making the history swappable would + probably break this. +""" +import datetime +import os +import threading +from abc import ABCMeta, abstractmethod +from typing import AsyncGenerator, Iterable, List, Optional, Sequence, Tuple + +from prompt_toolkit.eventloop import get_event_loop + +__all__ = [ + "History", + "ThreadedHistory", + "DummyHistory", + "FileHistory", + "InMemoryHistory", +] + + +class History(metaclass=ABCMeta): + """ + Base ``History`` class. + + This also includes abstract methods for loading/storing history. + """ + + def __init__(self) -> None: + # In memory storage for strings. + self._loaded = False + + # History that's loaded already, in reverse order. Latest, most recent + # item first. + self._loaded_strings: List[str] = [] + + # + # Methods expected by `Buffer`. + # + + async def load(self) -> AsyncGenerator[str, None]: + """ + Load the history and yield all the entries in reverse order (latest, + most recent history entry first). + + This method can be called multiple times from the `Buffer` to + repopulate the history when prompting for a new input. So we are + responsible here for both caching, and making sure that strings that + were were appended to the history will be incorporated next time this + method is called. + """ + if not self._loaded: + self._loaded_strings = list(self.load_history_strings()) + self._loaded = True + + for item in self._loaded_strings: + yield item + + def get_strings(self) -> List[str]: + """ + Get the strings from the history that are loaded so far. + (In order. Oldest item first.) + """ + return self._loaded_strings[::-1] + + def append_string(self, string: str) -> None: + "Add string to the history." + self._loaded_strings.insert(0, string) + self.store_string(string) + + # + # Implementation for specific backends. + # + + @abstractmethod + def load_history_strings(self) -> Iterable[str]: + """ + This should be a generator that yields `str` instances. + + It should yield the most recent items first, because they are the most + important. (The history can already be used, even when it's only + partially loaded.) + """ + while False: + yield + + @abstractmethod + def store_string(self, string: str) -> None: + """ + Store the string in persistent storage. + """ + + +class ThreadedHistory(History): + """ + Wrapper around `History` implementations that run the `load()` generator in + a thread. + + Use this to increase the start-up time of prompt_toolkit applications. + History entries are available as soon as they are loaded. We don't have to + wait for everything to be loaded. + """ + + def __init__(self, history: History) -> None: + super().__init__() + + self.history = history + + self._load_thread: Optional[threading.Thread] = None + + # Lock for accessing/manipulating `_loaded_strings` and `_loaded` + # together in a consistent state. + self._lock = threading.Lock() + + # Events created by each `load()` call. Used to wait for new history + # entries from the loader thread. + self._string_load_events: List[threading.Event] = [] + + async def load(self) -> AsyncGenerator[str, None]: + """ + Like `History.load(), but call `self.load_history_strings()` in a + background thread. + """ + # Start the load thread, if this is called for the first time. + if not self._load_thread: + self._load_thread = threading.Thread( + target=self._in_load_thread, + daemon=True, + ) + self._load_thread.start() + + # Consume the `_loaded_strings` list, using asyncio. + loop = get_event_loop() + + # Create threading Event so that we can wait for new items. + event = threading.Event() + event.set() + self._string_load_events.append(event) + + items_yielded = 0 + + try: + while True: + # Wait for new items to be available. + # (Use a timeout, because the executor thread is not a daemon + # thread. The "slow-history.py" example would otherwise hang if + # Control-C is pressed before the history is fully loaded, + # because there's still this non-daemon executor thread waiting + # for this event.) + got_timeout = await loop.run_in_executor( + None, lambda: event.wait(timeout=0.5) + ) + if not got_timeout: + continue + + # Read new items (in lock). + def in_executor() -> Tuple[List[str], bool]: + with self._lock: + new_items = self._loaded_strings[items_yielded:] + done = self._loaded + event.clear() + return new_items, done + + new_items, done = await loop.run_in_executor(None, in_executor) + + items_yielded += len(new_items) + + for item in new_items: + yield item + + if done: + break + finally: + self._string_load_events.remove(event) + + def _in_load_thread(self) -> None: + try: + # Start with an empty list. In case `append_string()` was called + # before `load()` happened. Then `.store_string()` will have + # written these entries back to disk and we will reload it. + self._loaded_strings = [] + + for item in self.history.load_history_strings(): + with self._lock: + self._loaded_strings.append(item) + + for event in self._string_load_events: + event.set() + finally: + with self._lock: + self._loaded = True + for event in self._string_load_events: + event.set() + + def append_string(self, string: str) -> None: + with self._lock: + self._loaded_strings.insert(0, string) + self.store_string(string) + + # All of the following are proxied to `self.history`. + + def load_history_strings(self) -> Iterable[str]: + return self.history.load_history_strings() + + def store_string(self, string: str) -> None: + self.history.store_string(string) + + def __repr__(self) -> str: + return "ThreadedHistory(%r)" % (self.history,) + + +class InMemoryHistory(History): + """ + :class:`.History` class that keeps a list of all strings in memory. + + In order to prepopulate the history, it's possible to call either + `append_string` for all items or pass a list of strings to `__init__` here. + """ + + def __init__(self, history_strings: Optional[Sequence[str]] = None) -> None: + super().__init__() + # Emulating disk storage. + if history_strings is None: + self._storage = [] + else: + self._storage = list(history_strings) + + def load_history_strings(self) -> Iterable[str]: + for item in self._storage[::-1]: + yield item + + def store_string(self, string: str) -> None: + self._storage.append(string) + + +class DummyHistory(History): + """ + :class:`.History` object that doesn't remember anything. + """ + + def load_history_strings(self) -> Iterable[str]: + return [] + + def store_string(self, string: str) -> None: + pass + + def append_string(self, string: str) -> None: + # Don't remember this. + pass + + +class FileHistory(History): + """ + :class:`.History` class that stores all strings in a file. + """ + + def __init__(self, filename: str) -> None: + self.filename = filename + super(FileHistory, self).__init__() + + def load_history_strings(self) -> Iterable[str]: + strings: List[str] = [] + lines: List[str] = [] + + def add() -> None: + if lines: + # Join and drop trailing newline. + string = "".join(lines)[:-1] + + strings.append(string) + + if os.path.exists(self.filename): + with open(self.filename, "rb") as f: + for line_bytes in f: + line = line_bytes.decode("utf-8", errors="replace") + + if line.startswith("+"): + lines.append(line[1:]) + else: + add() + lines = [] + + add() + + # Reverse the order, because newest items have to go first. + return reversed(strings) + + def store_string(self, string: str) -> None: + # Save to file. + with open(self.filename, "ab") as f: + + def write(t: str) -> None: + f.write(t.encode("utf-8")) + + write("\n# %s\n" % datetime.datetime.now()) + for line in string.split("\n"): + write("+%s\n" % line) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/__init__.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/__init__.py index b3a8219d81..421d4ccdf4 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/__init__.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/__init__.py @@ -1,11 +1,11 @@ -from .base import DummyInput, Input -from .defaults import create_input, create_pipe_input - -__all__ = [ - # Base. - "Input", - "DummyInput", - # Defaults. - "create_input", - "create_pipe_input", -] +from .base import DummyInput, Input +from .defaults import create_input, create_pipe_input + +__all__ = [ + # Base. + "Input", + "DummyInput", + # Defaults. + "create_input", + "create_pipe_input", +] diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/ansi_escape_sequences.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/ansi_escape_sequences.py index 22006fdb5c..2e6c5b9b28 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/ansi_escape_sequences.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/ansi_escape_sequences.py @@ -1,126 +1,126 @@ -""" -Mappings from VT100 (ANSI) escape sequences to the corresponding prompt_toolkit -keys. - -We are not using the terminfo/termcap databases to detect the ANSI escape -sequences for the input. Instead, we recognize 99% of the most common -sequences. This works well, because in practice, every modern terminal is -mostly Xterm compatible. - -Some useful docs: -- Mintty: https://github.com/mintty/mintty/blob/master/wiki/Keycodes.md -""" -from typing import Dict, Tuple, Union - -from ..keys import Keys - -__all__ = [ - "ANSI_SEQUENCES", - "REVERSE_ANSI_SEQUENCES", -] - -# Mapping of vt100 escape codes to Keys. -ANSI_SEQUENCES: Dict[str, Union[Keys, Tuple[Keys, ...]]] = { - # Control keys. - "\x00": Keys.ControlAt, # Control-At (Also for Ctrl-Space) - "\x01": Keys.ControlA, # Control-A (home) - "\x02": Keys.ControlB, # Control-B (emacs cursor left) - "\x03": Keys.ControlC, # Control-C (interrupt) - "\x04": Keys.ControlD, # Control-D (exit) - "\x05": Keys.ControlE, # Control-E (end) - "\x06": Keys.ControlF, # Control-F (cursor forward) - "\x07": Keys.ControlG, # Control-G - "\x08": Keys.ControlH, # Control-H (8) (Identical to '\b') - "\x09": Keys.ControlI, # Control-I (9) (Identical to '\t') - "\x0a": Keys.ControlJ, # Control-J (10) (Identical to '\n') - "\x0b": Keys.ControlK, # Control-K (delete until end of line; vertical tab) - "\x0c": Keys.ControlL, # Control-L (clear; form feed) - "\x0d": Keys.ControlM, # Control-M (13) (Identical to '\r') - "\x0e": Keys.ControlN, # Control-N (14) (history forward) - "\x0f": Keys.ControlO, # Control-O (15) - "\x10": Keys.ControlP, # Control-P (16) (history back) - "\x11": Keys.ControlQ, # Control-Q - "\x12": Keys.ControlR, # Control-R (18) (reverse search) - "\x13": Keys.ControlS, # Control-S (19) (forward search) - "\x14": Keys.ControlT, # Control-T - "\x15": Keys.ControlU, # Control-U - "\x16": Keys.ControlV, # Control-V - "\x17": Keys.ControlW, # Control-W - "\x18": Keys.ControlX, # Control-X - "\x19": Keys.ControlY, # Control-Y (25) - "\x1a": Keys.ControlZ, # Control-Z - "\x1b": Keys.Escape, # Also Control-[ - "\x9b": Keys.ShiftEscape, - "\x1c": Keys.ControlBackslash, # Both Control-\ (also Ctrl-| ) - "\x1d": Keys.ControlSquareClose, # Control-] - "\x1e": Keys.ControlCircumflex, # Control-^ - "\x1f": Keys.ControlUnderscore, # Control-underscore (Also for Ctrl-hyphen.) - # ASCII Delete (0x7f) - # Vt220 (and Linux terminal) send this when pressing backspace. We map this - # to ControlH, because that will make it easier to create key bindings that - # work everywhere, with the trade-off that it's no longer possible to - # handle backspace and control-h individually for the few terminals that - # support it. (Most terminals send ControlH when backspace is pressed.) - # See: http://www.ibb.net/~anne/keyboard.html - "\x7f": Keys.ControlH, - # -- - # Various - "\x1b[1~": Keys.Home, # tmux - "\x1b[2~": Keys.Insert, - "\x1b[3~": Keys.Delete, - "\x1b[4~": Keys.End, # tmux - "\x1b[5~": Keys.PageUp, - "\x1b[6~": Keys.PageDown, - "\x1b[7~": Keys.Home, # xrvt - "\x1b[8~": Keys.End, # xrvt - "\x1b[Z": Keys.BackTab, # shift + tab - "\x1b\x09": Keys.BackTab, # Linux console - "\x1b[~": Keys.BackTab, # Windows console - # -- - # Function keys. - "\x1bOP": Keys.F1, - "\x1bOQ": Keys.F2, - "\x1bOR": Keys.F3, - "\x1bOS": Keys.F4, - "\x1b[[A": Keys.F1, # Linux console. - "\x1b[[B": Keys.F2, # Linux console. - "\x1b[[C": Keys.F3, # Linux console. - "\x1b[[D": Keys.F4, # Linux console. - "\x1b[[E": Keys.F5, # Linux console. - "\x1b[11~": Keys.F1, # rxvt-unicode - "\x1b[12~": Keys.F2, # rxvt-unicode - "\x1b[13~": Keys.F3, # rxvt-unicode - "\x1b[14~": Keys.F4, # rxvt-unicode - "\x1b[15~": Keys.F5, - "\x1b[17~": Keys.F6, - "\x1b[18~": Keys.F7, - "\x1b[19~": Keys.F8, - "\x1b[20~": Keys.F9, - "\x1b[21~": Keys.F10, - "\x1b[23~": Keys.F11, - "\x1b[24~": Keys.F12, - "\x1b[25~": Keys.F13, - "\x1b[26~": Keys.F14, - "\x1b[28~": Keys.F15, - "\x1b[29~": Keys.F16, - "\x1b[31~": Keys.F17, - "\x1b[32~": Keys.F18, - "\x1b[33~": Keys.F19, - "\x1b[34~": Keys.F20, - # Xterm - "\x1b[1;2P": Keys.F13, - "\x1b[1;2Q": Keys.F14, - # '\x1b[1;2R': Keys.F15, # Conflicts with CPR response. - "\x1b[1;2S": Keys.F16, - "\x1b[15;2~": Keys.F17, - "\x1b[17;2~": Keys.F18, - "\x1b[18;2~": Keys.F19, - "\x1b[19;2~": Keys.F20, - "\x1b[20;2~": Keys.F21, - "\x1b[21;2~": Keys.F22, - "\x1b[23;2~": Keys.F23, - "\x1b[24;2~": Keys.F24, - # -- +""" +Mappings from VT100 (ANSI) escape sequences to the corresponding prompt_toolkit +keys. + +We are not using the terminfo/termcap databases to detect the ANSI escape +sequences for the input. Instead, we recognize 99% of the most common +sequences. This works well, because in practice, every modern terminal is +mostly Xterm compatible. + +Some useful docs: +- Mintty: https://github.com/mintty/mintty/blob/master/wiki/Keycodes.md +""" +from typing import Dict, Tuple, Union + +from ..keys import Keys + +__all__ = [ + "ANSI_SEQUENCES", + "REVERSE_ANSI_SEQUENCES", +] + +# Mapping of vt100 escape codes to Keys. +ANSI_SEQUENCES: Dict[str, Union[Keys, Tuple[Keys, ...]]] = { + # Control keys. + "\x00": Keys.ControlAt, # Control-At (Also for Ctrl-Space) + "\x01": Keys.ControlA, # Control-A (home) + "\x02": Keys.ControlB, # Control-B (emacs cursor left) + "\x03": Keys.ControlC, # Control-C (interrupt) + "\x04": Keys.ControlD, # Control-D (exit) + "\x05": Keys.ControlE, # Control-E (end) + "\x06": Keys.ControlF, # Control-F (cursor forward) + "\x07": Keys.ControlG, # Control-G + "\x08": Keys.ControlH, # Control-H (8) (Identical to '\b') + "\x09": Keys.ControlI, # Control-I (9) (Identical to '\t') + "\x0a": Keys.ControlJ, # Control-J (10) (Identical to '\n') + "\x0b": Keys.ControlK, # Control-K (delete until end of line; vertical tab) + "\x0c": Keys.ControlL, # Control-L (clear; form feed) + "\x0d": Keys.ControlM, # Control-M (13) (Identical to '\r') + "\x0e": Keys.ControlN, # Control-N (14) (history forward) + "\x0f": Keys.ControlO, # Control-O (15) + "\x10": Keys.ControlP, # Control-P (16) (history back) + "\x11": Keys.ControlQ, # Control-Q + "\x12": Keys.ControlR, # Control-R (18) (reverse search) + "\x13": Keys.ControlS, # Control-S (19) (forward search) + "\x14": Keys.ControlT, # Control-T + "\x15": Keys.ControlU, # Control-U + "\x16": Keys.ControlV, # Control-V + "\x17": Keys.ControlW, # Control-W + "\x18": Keys.ControlX, # Control-X + "\x19": Keys.ControlY, # Control-Y (25) + "\x1a": Keys.ControlZ, # Control-Z + "\x1b": Keys.Escape, # Also Control-[ + "\x9b": Keys.ShiftEscape, + "\x1c": Keys.ControlBackslash, # Both Control-\ (also Ctrl-| ) + "\x1d": Keys.ControlSquareClose, # Control-] + "\x1e": Keys.ControlCircumflex, # Control-^ + "\x1f": Keys.ControlUnderscore, # Control-underscore (Also for Ctrl-hyphen.) + # ASCII Delete (0x7f) + # Vt220 (and Linux terminal) send this when pressing backspace. We map this + # to ControlH, because that will make it easier to create key bindings that + # work everywhere, with the trade-off that it's no longer possible to + # handle backspace and control-h individually for the few terminals that + # support it. (Most terminals send ControlH when backspace is pressed.) + # See: http://www.ibb.net/~anne/keyboard.html + "\x7f": Keys.ControlH, + # -- + # Various + "\x1b[1~": Keys.Home, # tmux + "\x1b[2~": Keys.Insert, + "\x1b[3~": Keys.Delete, + "\x1b[4~": Keys.End, # tmux + "\x1b[5~": Keys.PageUp, + "\x1b[6~": Keys.PageDown, + "\x1b[7~": Keys.Home, # xrvt + "\x1b[8~": Keys.End, # xrvt + "\x1b[Z": Keys.BackTab, # shift + tab + "\x1b\x09": Keys.BackTab, # Linux console + "\x1b[~": Keys.BackTab, # Windows console + # -- + # Function keys. + "\x1bOP": Keys.F1, + "\x1bOQ": Keys.F2, + "\x1bOR": Keys.F3, + "\x1bOS": Keys.F4, + "\x1b[[A": Keys.F1, # Linux console. + "\x1b[[B": Keys.F2, # Linux console. + "\x1b[[C": Keys.F3, # Linux console. + "\x1b[[D": Keys.F4, # Linux console. + "\x1b[[E": Keys.F5, # Linux console. + "\x1b[11~": Keys.F1, # rxvt-unicode + "\x1b[12~": Keys.F2, # rxvt-unicode + "\x1b[13~": Keys.F3, # rxvt-unicode + "\x1b[14~": Keys.F4, # rxvt-unicode + "\x1b[15~": Keys.F5, + "\x1b[17~": Keys.F6, + "\x1b[18~": Keys.F7, + "\x1b[19~": Keys.F8, + "\x1b[20~": Keys.F9, + "\x1b[21~": Keys.F10, + "\x1b[23~": Keys.F11, + "\x1b[24~": Keys.F12, + "\x1b[25~": Keys.F13, + "\x1b[26~": Keys.F14, + "\x1b[28~": Keys.F15, + "\x1b[29~": Keys.F16, + "\x1b[31~": Keys.F17, + "\x1b[32~": Keys.F18, + "\x1b[33~": Keys.F19, + "\x1b[34~": Keys.F20, + # Xterm + "\x1b[1;2P": Keys.F13, + "\x1b[1;2Q": Keys.F14, + # '\x1b[1;2R': Keys.F15, # Conflicts with CPR response. + "\x1b[1;2S": Keys.F16, + "\x1b[15;2~": Keys.F17, + "\x1b[17;2~": Keys.F18, + "\x1b[18;2~": Keys.F19, + "\x1b[19;2~": Keys.F20, + "\x1b[20;2~": Keys.F21, + "\x1b[21;2~": Keys.F22, + "\x1b[23;2~": Keys.F23, + "\x1b[24;2~": Keys.F24, + # -- # CSI 27 disambiguated modified "other" keys (xterm) # Ref: https://invisible-island.net/xterm/modified-keys.html # These are currently unsupported, so just re-map some common ones to the @@ -129,215 +129,215 @@ ANSI_SEQUENCES: Dict[str, Union[Keys, Tuple[Keys, ...]]] = { "\x1b[27;5;13~": Keys.ControlM, # Ctrl + Enter "\x1b[27;6;13~": Keys.ControlM, # Ctrl + Shift + Enter # -- - # Control + function keys. - "\x1b[1;5P": Keys.ControlF1, - "\x1b[1;5Q": Keys.ControlF2, - # "\x1b[1;5R": Keys.ControlF3, # Conflicts with CPR response. - "\x1b[1;5S": Keys.ControlF4, - "\x1b[15;5~": Keys.ControlF5, - "\x1b[17;5~": Keys.ControlF6, - "\x1b[18;5~": Keys.ControlF7, - "\x1b[19;5~": Keys.ControlF8, - "\x1b[20;5~": Keys.ControlF9, - "\x1b[21;5~": Keys.ControlF10, - "\x1b[23;5~": Keys.ControlF11, - "\x1b[24;5~": Keys.ControlF12, - "\x1b[1;6P": Keys.ControlF13, - "\x1b[1;6Q": Keys.ControlF14, - # "\x1b[1;6R": Keys.ControlF15, # Conflicts with CPR response. - "\x1b[1;6S": Keys.ControlF16, - "\x1b[15;6~": Keys.ControlF17, - "\x1b[17;6~": Keys.ControlF18, - "\x1b[18;6~": Keys.ControlF19, - "\x1b[19;6~": Keys.ControlF20, - "\x1b[20;6~": Keys.ControlF21, - "\x1b[21;6~": Keys.ControlF22, - "\x1b[23;6~": Keys.ControlF23, - "\x1b[24;6~": Keys.ControlF24, - # -- - # Tmux (Win32 subsystem) sends the following scroll events. - "\x1b[62~": Keys.ScrollUp, - "\x1b[63~": Keys.ScrollDown, - "\x1b[200~": Keys.BracketedPaste, # Start of bracketed paste. - # -- - # Sequences generated by numpad 5. Not sure what it means. (It doesn't - # appear in 'infocmp'. Just ignore. - "\x1b[E": Keys.Ignore, # Xterm. - "\x1b[G": Keys.Ignore, # Linux console. - # -- - # Meta/control/escape + pageup/pagedown/insert/delete. - "\x1b[3;2~": Keys.ShiftDelete, # xterm, gnome-terminal. - "\x1b[5;2~": Keys.ShiftPageUp, - "\x1b[6;2~": Keys.ShiftPageDown, - "\x1b[2;3~": (Keys.Escape, Keys.Insert), - "\x1b[3;3~": (Keys.Escape, Keys.Delete), - "\x1b[5;3~": (Keys.Escape, Keys.PageUp), - "\x1b[6;3~": (Keys.Escape, Keys.PageDown), - "\x1b[2;4~": (Keys.Escape, Keys.ShiftInsert), - "\x1b[3;4~": (Keys.Escape, Keys.ShiftDelete), - "\x1b[5;4~": (Keys.Escape, Keys.ShiftPageUp), - "\x1b[6;4~": (Keys.Escape, Keys.ShiftPageDown), - "\x1b[3;5~": Keys.ControlDelete, # xterm, gnome-terminal. - "\x1b[5;5~": Keys.ControlPageUp, - "\x1b[6;5~": Keys.ControlPageDown, - "\x1b[3;6~": Keys.ControlShiftDelete, - "\x1b[5;6~": Keys.ControlShiftPageUp, - "\x1b[6;6~": Keys.ControlShiftPageDown, - "\x1b[2;7~": (Keys.Escape, Keys.ControlInsert), - "\x1b[5;7~": (Keys.Escape, Keys.ControlPageDown), - "\x1b[6;7~": (Keys.Escape, Keys.ControlPageDown), - "\x1b[2;8~": (Keys.Escape, Keys.ControlShiftInsert), - "\x1b[5;8~": (Keys.Escape, Keys.ControlShiftPageDown), - "\x1b[6;8~": (Keys.Escape, Keys.ControlShiftPageDown), - # -- - # Arrows. - # (Normal cursor mode). - "\x1b[A": Keys.Up, - "\x1b[B": Keys.Down, - "\x1b[C": Keys.Right, - "\x1b[D": Keys.Left, - "\x1b[H": Keys.Home, - "\x1b[F": Keys.End, - # Tmux sends following keystrokes when control+arrow is pressed, but for - # Emacs ansi-term sends the same sequences for normal arrow keys. Consider - # it a normal arrow press, because that's more important. - # (Application cursor mode). - "\x1bOA": Keys.Up, - "\x1bOB": Keys.Down, - "\x1bOC": Keys.Right, - "\x1bOD": Keys.Left, - "\x1bOF": Keys.End, - "\x1bOH": Keys.Home, - # Shift + arrows. - "\x1b[1;2A": Keys.ShiftUp, - "\x1b[1;2B": Keys.ShiftDown, - "\x1b[1;2C": Keys.ShiftRight, - "\x1b[1;2D": Keys.ShiftLeft, - "\x1b[1;2F": Keys.ShiftEnd, - "\x1b[1;2H": Keys.ShiftHome, - # Meta + arrow keys. Several terminals handle this differently. - # The following sequences are for xterm and gnome-terminal. - # (Iterm sends ESC followed by the normal arrow_up/down/left/right - # sequences, and the OSX Terminal sends ESCb and ESCf for "alt - # arrow_left" and "alt arrow_right." We don't handle these - # explicitly, in here, because would could not distinguish between - # pressing ESC (to go to Vi navigation mode), followed by just the - # 'b' or 'f' key. These combinations are handled in - # the input processor.) - "\x1b[1;3A": (Keys.Escape, Keys.Up), - "\x1b[1;3B": (Keys.Escape, Keys.Down), - "\x1b[1;3C": (Keys.Escape, Keys.Right), - "\x1b[1;3D": (Keys.Escape, Keys.Left), - "\x1b[1;3F": (Keys.Escape, Keys.End), - "\x1b[1;3H": (Keys.Escape, Keys.Home), - # Alt+shift+number. - "\x1b[1;4A": (Keys.Escape, Keys.ShiftDown), - "\x1b[1;4B": (Keys.Escape, Keys.ShiftUp), - "\x1b[1;4C": (Keys.Escape, Keys.ShiftRight), - "\x1b[1;4D": (Keys.Escape, Keys.ShiftLeft), - "\x1b[1;4F": (Keys.Escape, Keys.ShiftEnd), - "\x1b[1;4H": (Keys.Escape, Keys.ShiftHome), - # Control + arrows. - "\x1b[1;5A": Keys.ControlUp, # Cursor Mode - "\x1b[1;5B": Keys.ControlDown, # Cursor Mode - "\x1b[1;5C": Keys.ControlRight, # Cursor Mode - "\x1b[1;5D": Keys.ControlLeft, # Cursor Mode - "\x1b[1;5F": Keys.ControlEnd, - "\x1b[1;5H": Keys.ControlHome, - # Tmux sends following keystrokes when control+arrow is pressed, but for - # Emacs ansi-term sends the same sequences for normal arrow keys. Consider - # it a normal arrow press, because that's more important. - "\x1b[5A": Keys.ControlUp, - "\x1b[5B": Keys.ControlDown, - "\x1b[5C": Keys.ControlRight, - "\x1b[5D": Keys.ControlLeft, - "\x1bOc": Keys.ControlRight, # rxvt - "\x1bOd": Keys.ControlLeft, # rxvt - # Control + shift + arrows. - "\x1b[1;6A": Keys.ControlShiftDown, - "\x1b[1;6B": Keys.ControlShiftUp, - "\x1b[1;6C": Keys.ControlShiftRight, - "\x1b[1;6D": Keys.ControlShiftLeft, - "\x1b[1;6F": Keys.ControlShiftEnd, - "\x1b[1;6H": Keys.ControlShiftHome, - # Control + Meta + arrows. - "\x1b[1;7A": (Keys.Escape, Keys.ControlDown), - "\x1b[1;7B": (Keys.Escape, Keys.ControlUp), - "\x1b[1;7C": (Keys.Escape, Keys.ControlRight), - "\x1b[1;7D": (Keys.Escape, Keys.ControlLeft), - "\x1b[1;7F": (Keys.Escape, Keys.ControlEnd), - "\x1b[1;7H": (Keys.Escape, Keys.ControlHome), - # Meta + Shift + arrows. - "\x1b[1;8A": (Keys.Escape, Keys.ControlShiftDown), - "\x1b[1;8B": (Keys.Escape, Keys.ControlShiftUp), - "\x1b[1;8C": (Keys.Escape, Keys.ControlShiftRight), - "\x1b[1;8D": (Keys.Escape, Keys.ControlShiftLeft), - "\x1b[1;8F": (Keys.Escape, Keys.ControlShiftEnd), - "\x1b[1;8H": (Keys.Escape, Keys.ControlShiftHome), - # Meta + arrow on (some?) Macs when using iTerm defaults (see issue #483). - "\x1b[1;9A": (Keys.Escape, Keys.Up), - "\x1b[1;9B": (Keys.Escape, Keys.Down), - "\x1b[1;9C": (Keys.Escape, Keys.Right), - "\x1b[1;9D": (Keys.Escape, Keys.Left), - # -- - # Control/shift/meta + number in mintty. - # (c-2 will actually send c-@ and c-6 will send c-^.) - "\x1b[1;5p": Keys.Control0, - "\x1b[1;5q": Keys.Control1, - "\x1b[1;5r": Keys.Control2, - "\x1b[1;5s": Keys.Control3, - "\x1b[1;5t": Keys.Control4, - "\x1b[1;5u": Keys.Control5, - "\x1b[1;5v": Keys.Control6, - "\x1b[1;5w": Keys.Control7, - "\x1b[1;5x": Keys.Control8, - "\x1b[1;5y": Keys.Control9, - "\x1b[1;6p": Keys.ControlShift0, - "\x1b[1;6q": Keys.ControlShift1, - "\x1b[1;6r": Keys.ControlShift2, - "\x1b[1;6s": Keys.ControlShift3, - "\x1b[1;6t": Keys.ControlShift4, - "\x1b[1;6u": Keys.ControlShift5, - "\x1b[1;6v": Keys.ControlShift6, - "\x1b[1;6w": Keys.ControlShift7, - "\x1b[1;6x": Keys.ControlShift8, - "\x1b[1;6y": Keys.ControlShift9, - "\x1b[1;7p": (Keys.Escape, Keys.Control0), - "\x1b[1;7q": (Keys.Escape, Keys.Control1), - "\x1b[1;7r": (Keys.Escape, Keys.Control2), - "\x1b[1;7s": (Keys.Escape, Keys.Control3), - "\x1b[1;7t": (Keys.Escape, Keys.Control4), - "\x1b[1;7u": (Keys.Escape, Keys.Control5), - "\x1b[1;7v": (Keys.Escape, Keys.Control6), - "\x1b[1;7w": (Keys.Escape, Keys.Control7), - "\x1b[1;7x": (Keys.Escape, Keys.Control8), - "\x1b[1;7y": (Keys.Escape, Keys.Control9), - "\x1b[1;8p": (Keys.Escape, Keys.ControlShift0), - "\x1b[1;8q": (Keys.Escape, Keys.ControlShift1), - "\x1b[1;8r": (Keys.Escape, Keys.ControlShift2), - "\x1b[1;8s": (Keys.Escape, Keys.ControlShift3), - "\x1b[1;8t": (Keys.Escape, Keys.ControlShift4), - "\x1b[1;8u": (Keys.Escape, Keys.ControlShift5), - "\x1b[1;8v": (Keys.Escape, Keys.ControlShift6), - "\x1b[1;8w": (Keys.Escape, Keys.ControlShift7), - "\x1b[1;8x": (Keys.Escape, Keys.ControlShift8), - "\x1b[1;8y": (Keys.Escape, Keys.ControlShift9), -} - - -def _get_reverse_ansi_sequences() -> Dict[Keys, str]: - """ - Create a dictionary that maps prompt_toolkit keys back to the VT100 escape - sequences. - """ - result: Dict[Keys, str] = {} - - for sequence, key in ANSI_SEQUENCES.items(): - if not isinstance(key, tuple): - if key not in result: - result[key] = sequence - - return result - - -REVERSE_ANSI_SEQUENCES = _get_reverse_ansi_sequences() + # Control + function keys. + "\x1b[1;5P": Keys.ControlF1, + "\x1b[1;5Q": Keys.ControlF2, + # "\x1b[1;5R": Keys.ControlF3, # Conflicts with CPR response. + "\x1b[1;5S": Keys.ControlF4, + "\x1b[15;5~": Keys.ControlF5, + "\x1b[17;5~": Keys.ControlF6, + "\x1b[18;5~": Keys.ControlF7, + "\x1b[19;5~": Keys.ControlF8, + "\x1b[20;5~": Keys.ControlF9, + "\x1b[21;5~": Keys.ControlF10, + "\x1b[23;5~": Keys.ControlF11, + "\x1b[24;5~": Keys.ControlF12, + "\x1b[1;6P": Keys.ControlF13, + "\x1b[1;6Q": Keys.ControlF14, + # "\x1b[1;6R": Keys.ControlF15, # Conflicts with CPR response. + "\x1b[1;6S": Keys.ControlF16, + "\x1b[15;6~": Keys.ControlF17, + "\x1b[17;6~": Keys.ControlF18, + "\x1b[18;6~": Keys.ControlF19, + "\x1b[19;6~": Keys.ControlF20, + "\x1b[20;6~": Keys.ControlF21, + "\x1b[21;6~": Keys.ControlF22, + "\x1b[23;6~": Keys.ControlF23, + "\x1b[24;6~": Keys.ControlF24, + # -- + # Tmux (Win32 subsystem) sends the following scroll events. + "\x1b[62~": Keys.ScrollUp, + "\x1b[63~": Keys.ScrollDown, + "\x1b[200~": Keys.BracketedPaste, # Start of bracketed paste. + # -- + # Sequences generated by numpad 5. Not sure what it means. (It doesn't + # appear in 'infocmp'. Just ignore. + "\x1b[E": Keys.Ignore, # Xterm. + "\x1b[G": Keys.Ignore, # Linux console. + # -- + # Meta/control/escape + pageup/pagedown/insert/delete. + "\x1b[3;2~": Keys.ShiftDelete, # xterm, gnome-terminal. + "\x1b[5;2~": Keys.ShiftPageUp, + "\x1b[6;2~": Keys.ShiftPageDown, + "\x1b[2;3~": (Keys.Escape, Keys.Insert), + "\x1b[3;3~": (Keys.Escape, Keys.Delete), + "\x1b[5;3~": (Keys.Escape, Keys.PageUp), + "\x1b[6;3~": (Keys.Escape, Keys.PageDown), + "\x1b[2;4~": (Keys.Escape, Keys.ShiftInsert), + "\x1b[3;4~": (Keys.Escape, Keys.ShiftDelete), + "\x1b[5;4~": (Keys.Escape, Keys.ShiftPageUp), + "\x1b[6;4~": (Keys.Escape, Keys.ShiftPageDown), + "\x1b[3;5~": Keys.ControlDelete, # xterm, gnome-terminal. + "\x1b[5;5~": Keys.ControlPageUp, + "\x1b[6;5~": Keys.ControlPageDown, + "\x1b[3;6~": Keys.ControlShiftDelete, + "\x1b[5;6~": Keys.ControlShiftPageUp, + "\x1b[6;6~": Keys.ControlShiftPageDown, + "\x1b[2;7~": (Keys.Escape, Keys.ControlInsert), + "\x1b[5;7~": (Keys.Escape, Keys.ControlPageDown), + "\x1b[6;7~": (Keys.Escape, Keys.ControlPageDown), + "\x1b[2;8~": (Keys.Escape, Keys.ControlShiftInsert), + "\x1b[5;8~": (Keys.Escape, Keys.ControlShiftPageDown), + "\x1b[6;8~": (Keys.Escape, Keys.ControlShiftPageDown), + # -- + # Arrows. + # (Normal cursor mode). + "\x1b[A": Keys.Up, + "\x1b[B": Keys.Down, + "\x1b[C": Keys.Right, + "\x1b[D": Keys.Left, + "\x1b[H": Keys.Home, + "\x1b[F": Keys.End, + # Tmux sends following keystrokes when control+arrow is pressed, but for + # Emacs ansi-term sends the same sequences for normal arrow keys. Consider + # it a normal arrow press, because that's more important. + # (Application cursor mode). + "\x1bOA": Keys.Up, + "\x1bOB": Keys.Down, + "\x1bOC": Keys.Right, + "\x1bOD": Keys.Left, + "\x1bOF": Keys.End, + "\x1bOH": Keys.Home, + # Shift + arrows. + "\x1b[1;2A": Keys.ShiftUp, + "\x1b[1;2B": Keys.ShiftDown, + "\x1b[1;2C": Keys.ShiftRight, + "\x1b[1;2D": Keys.ShiftLeft, + "\x1b[1;2F": Keys.ShiftEnd, + "\x1b[1;2H": Keys.ShiftHome, + # Meta + arrow keys. Several terminals handle this differently. + # The following sequences are for xterm and gnome-terminal. + # (Iterm sends ESC followed by the normal arrow_up/down/left/right + # sequences, and the OSX Terminal sends ESCb and ESCf for "alt + # arrow_left" and "alt arrow_right." We don't handle these + # explicitly, in here, because would could not distinguish between + # pressing ESC (to go to Vi navigation mode), followed by just the + # 'b' or 'f' key. These combinations are handled in + # the input processor.) + "\x1b[1;3A": (Keys.Escape, Keys.Up), + "\x1b[1;3B": (Keys.Escape, Keys.Down), + "\x1b[1;3C": (Keys.Escape, Keys.Right), + "\x1b[1;3D": (Keys.Escape, Keys.Left), + "\x1b[1;3F": (Keys.Escape, Keys.End), + "\x1b[1;3H": (Keys.Escape, Keys.Home), + # Alt+shift+number. + "\x1b[1;4A": (Keys.Escape, Keys.ShiftDown), + "\x1b[1;4B": (Keys.Escape, Keys.ShiftUp), + "\x1b[1;4C": (Keys.Escape, Keys.ShiftRight), + "\x1b[1;4D": (Keys.Escape, Keys.ShiftLeft), + "\x1b[1;4F": (Keys.Escape, Keys.ShiftEnd), + "\x1b[1;4H": (Keys.Escape, Keys.ShiftHome), + # Control + arrows. + "\x1b[1;5A": Keys.ControlUp, # Cursor Mode + "\x1b[1;5B": Keys.ControlDown, # Cursor Mode + "\x1b[1;5C": Keys.ControlRight, # Cursor Mode + "\x1b[1;5D": Keys.ControlLeft, # Cursor Mode + "\x1b[1;5F": Keys.ControlEnd, + "\x1b[1;5H": Keys.ControlHome, + # Tmux sends following keystrokes when control+arrow is pressed, but for + # Emacs ansi-term sends the same sequences for normal arrow keys. Consider + # it a normal arrow press, because that's more important. + "\x1b[5A": Keys.ControlUp, + "\x1b[5B": Keys.ControlDown, + "\x1b[5C": Keys.ControlRight, + "\x1b[5D": Keys.ControlLeft, + "\x1bOc": Keys.ControlRight, # rxvt + "\x1bOd": Keys.ControlLeft, # rxvt + # Control + shift + arrows. + "\x1b[1;6A": Keys.ControlShiftDown, + "\x1b[1;6B": Keys.ControlShiftUp, + "\x1b[1;6C": Keys.ControlShiftRight, + "\x1b[1;6D": Keys.ControlShiftLeft, + "\x1b[1;6F": Keys.ControlShiftEnd, + "\x1b[1;6H": Keys.ControlShiftHome, + # Control + Meta + arrows. + "\x1b[1;7A": (Keys.Escape, Keys.ControlDown), + "\x1b[1;7B": (Keys.Escape, Keys.ControlUp), + "\x1b[1;7C": (Keys.Escape, Keys.ControlRight), + "\x1b[1;7D": (Keys.Escape, Keys.ControlLeft), + "\x1b[1;7F": (Keys.Escape, Keys.ControlEnd), + "\x1b[1;7H": (Keys.Escape, Keys.ControlHome), + # Meta + Shift + arrows. + "\x1b[1;8A": (Keys.Escape, Keys.ControlShiftDown), + "\x1b[1;8B": (Keys.Escape, Keys.ControlShiftUp), + "\x1b[1;8C": (Keys.Escape, Keys.ControlShiftRight), + "\x1b[1;8D": (Keys.Escape, Keys.ControlShiftLeft), + "\x1b[1;8F": (Keys.Escape, Keys.ControlShiftEnd), + "\x1b[1;8H": (Keys.Escape, Keys.ControlShiftHome), + # Meta + arrow on (some?) Macs when using iTerm defaults (see issue #483). + "\x1b[1;9A": (Keys.Escape, Keys.Up), + "\x1b[1;9B": (Keys.Escape, Keys.Down), + "\x1b[1;9C": (Keys.Escape, Keys.Right), + "\x1b[1;9D": (Keys.Escape, Keys.Left), + # -- + # Control/shift/meta + number in mintty. + # (c-2 will actually send c-@ and c-6 will send c-^.) + "\x1b[1;5p": Keys.Control0, + "\x1b[1;5q": Keys.Control1, + "\x1b[1;5r": Keys.Control2, + "\x1b[1;5s": Keys.Control3, + "\x1b[1;5t": Keys.Control4, + "\x1b[1;5u": Keys.Control5, + "\x1b[1;5v": Keys.Control6, + "\x1b[1;5w": Keys.Control7, + "\x1b[1;5x": Keys.Control8, + "\x1b[1;5y": Keys.Control9, + "\x1b[1;6p": Keys.ControlShift0, + "\x1b[1;6q": Keys.ControlShift1, + "\x1b[1;6r": Keys.ControlShift2, + "\x1b[1;6s": Keys.ControlShift3, + "\x1b[1;6t": Keys.ControlShift4, + "\x1b[1;6u": Keys.ControlShift5, + "\x1b[1;6v": Keys.ControlShift6, + "\x1b[1;6w": Keys.ControlShift7, + "\x1b[1;6x": Keys.ControlShift8, + "\x1b[1;6y": Keys.ControlShift9, + "\x1b[1;7p": (Keys.Escape, Keys.Control0), + "\x1b[1;7q": (Keys.Escape, Keys.Control1), + "\x1b[1;7r": (Keys.Escape, Keys.Control2), + "\x1b[1;7s": (Keys.Escape, Keys.Control3), + "\x1b[1;7t": (Keys.Escape, Keys.Control4), + "\x1b[1;7u": (Keys.Escape, Keys.Control5), + "\x1b[1;7v": (Keys.Escape, Keys.Control6), + "\x1b[1;7w": (Keys.Escape, Keys.Control7), + "\x1b[1;7x": (Keys.Escape, Keys.Control8), + "\x1b[1;7y": (Keys.Escape, Keys.Control9), + "\x1b[1;8p": (Keys.Escape, Keys.ControlShift0), + "\x1b[1;8q": (Keys.Escape, Keys.ControlShift1), + "\x1b[1;8r": (Keys.Escape, Keys.ControlShift2), + "\x1b[1;8s": (Keys.Escape, Keys.ControlShift3), + "\x1b[1;8t": (Keys.Escape, Keys.ControlShift4), + "\x1b[1;8u": (Keys.Escape, Keys.ControlShift5), + "\x1b[1;8v": (Keys.Escape, Keys.ControlShift6), + "\x1b[1;8w": (Keys.Escape, Keys.ControlShift7), + "\x1b[1;8x": (Keys.Escape, Keys.ControlShift8), + "\x1b[1;8y": (Keys.Escape, Keys.ControlShift9), +} + + +def _get_reverse_ansi_sequences() -> Dict[Keys, str]: + """ + Create a dictionary that maps prompt_toolkit keys back to the VT100 escape + sequences. + """ + result: Dict[Keys, str] = {} + + for sequence, key in ANSI_SEQUENCES.items(): + if not isinstance(key, tuple): + if key not in result: + result[key] = sequence + + return result + + +REVERSE_ANSI_SEQUENCES = _get_reverse_ansi_sequences() diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/base.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/base.py index e72f03a1e4..9885a37bc2 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/base.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/base.py @@ -1,131 +1,131 @@ -""" -Abstraction of CLI Input. -""" -from abc import ABCMeta, abstractmethod, abstractproperty -from contextlib import contextmanager -from typing import Callable, ContextManager, Generator, List - -from prompt_toolkit.key_binding import KeyPress - -__all__ = [ - "Input", - "DummyInput", -] - - -class Input(metaclass=ABCMeta): - """ - Abstraction for any input. - - An instance of this class can be given to the constructor of a - :class:`~prompt_toolkit.application.Application` and will also be - passed to the :class:`~prompt_toolkit.eventloop.base.EventLoop`. - """ - - @abstractmethod - def fileno(self) -> int: - """ - Fileno for putting this in an event loop. - """ - - @abstractmethod - def typeahead_hash(self) -> str: - """ - Identifier for storing type ahead key presses. - """ - - @abstractmethod - def read_keys(self) -> List[KeyPress]: - """ - Return a list of Key objects which are read/parsed from the input. - """ - - def flush_keys(self) -> List[KeyPress]: - """ - Flush the underlying parser. and return the pending keys. - (Used for vt100 input.) - """ - return [] - - def flush(self) -> None: - "The event loop can call this when the input has to be flushed." - pass - - @abstractproperty - def closed(self) -> bool: - "Should be true when the input stream is closed." - return False - - @abstractmethod - def raw_mode(self) -> ContextManager[None]: - """ - Context manager that turns the input into raw mode. - """ - - @abstractmethod - def cooked_mode(self) -> ContextManager[None]: - """ - Context manager that turns the input into cooked mode. - """ - - @abstractmethod - def attach(self, input_ready_callback: Callable[[], None]) -> ContextManager[None]: - """ - Return a context manager that makes this input active in the current - event loop. - """ - - @abstractmethod - def detach(self) -> ContextManager[None]: - """ - Return a context manager that makes sure that this input is not active - in the current event loop. - """ - - def close(self) -> None: - "Close input." - pass - - -class PipeInput(Input): - """ - Abstraction for pipe input. - """ - - @abstractmethod - def send_bytes(self, data: bytes) -> None: - """Feed byte string into the pipe""" - - @abstractmethod - def send_text(self, data: str) -> None: - """Feed a text string into the pipe""" - - -class DummyInput(Input): - """ - Input for use in a `DummyApplication` - """ - - def fileno(self) -> int: - raise NotImplementedError - - def typeahead_hash(self) -> str: - return "dummy-%s" % id(self) - - def read_keys(self) -> List[KeyPress]: - return [] - - @property - def closed(self) -> bool: - return True - - def raw_mode(self) -> ContextManager[None]: - return _dummy_context_manager() - - def cooked_mode(self) -> ContextManager[None]: - return _dummy_context_manager() - - def attach(self, input_ready_callback: Callable[[], None]) -> ContextManager[None]: +""" +Abstraction of CLI Input. +""" +from abc import ABCMeta, abstractmethod, abstractproperty +from contextlib import contextmanager +from typing import Callable, ContextManager, Generator, List + +from prompt_toolkit.key_binding import KeyPress + +__all__ = [ + "Input", + "DummyInput", +] + + +class Input(metaclass=ABCMeta): + """ + Abstraction for any input. + + An instance of this class can be given to the constructor of a + :class:`~prompt_toolkit.application.Application` and will also be + passed to the :class:`~prompt_toolkit.eventloop.base.EventLoop`. + """ + + @abstractmethod + def fileno(self) -> int: + """ + Fileno for putting this in an event loop. + """ + + @abstractmethod + def typeahead_hash(self) -> str: + """ + Identifier for storing type ahead key presses. + """ + + @abstractmethod + def read_keys(self) -> List[KeyPress]: + """ + Return a list of Key objects which are read/parsed from the input. + """ + + def flush_keys(self) -> List[KeyPress]: + """ + Flush the underlying parser. and return the pending keys. + (Used for vt100 input.) + """ + return [] + + def flush(self) -> None: + "The event loop can call this when the input has to be flushed." + pass + + @abstractproperty + def closed(self) -> bool: + "Should be true when the input stream is closed." + return False + + @abstractmethod + def raw_mode(self) -> ContextManager[None]: + """ + Context manager that turns the input into raw mode. + """ + + @abstractmethod + def cooked_mode(self) -> ContextManager[None]: + """ + Context manager that turns the input into cooked mode. + """ + + @abstractmethod + def attach(self, input_ready_callback: Callable[[], None]) -> ContextManager[None]: + """ + Return a context manager that makes this input active in the current + event loop. + """ + + @abstractmethod + def detach(self) -> ContextManager[None]: + """ + Return a context manager that makes sure that this input is not active + in the current event loop. + """ + + def close(self) -> None: + "Close input." + pass + + +class PipeInput(Input): + """ + Abstraction for pipe input. + """ + + @abstractmethod + def send_bytes(self, data: bytes) -> None: + """Feed byte string into the pipe""" + + @abstractmethod + def send_text(self, data: str) -> None: + """Feed a text string into the pipe""" + + +class DummyInput(Input): + """ + Input for use in a `DummyApplication` + """ + + def fileno(self) -> int: + raise NotImplementedError + + def typeahead_hash(self) -> str: + return "dummy-%s" % id(self) + + def read_keys(self) -> List[KeyPress]: + return [] + + @property + def closed(self) -> bool: + return True + + def raw_mode(self) -> ContextManager[None]: + return _dummy_context_manager() + + def cooked_mode(self) -> ContextManager[None]: + return _dummy_context_manager() + + def attach(self, input_ready_callback: Callable[[], None]) -> ContextManager[None]: # Call the callback immediately once after attaching. # This tells the callback to call `read_keys` and check the # `input.closed` flag, after which it won't receive any keys, but knows @@ -133,12 +133,12 @@ class DummyInput(Input): # `application.py`. input_ready_callback() - return _dummy_context_manager() - - def detach(self) -> ContextManager[None]: - return _dummy_context_manager() - - -@contextmanager -def _dummy_context_manager() -> Generator[None, None, None]: - yield + return _dummy_context_manager() + + def detach(self) -> ContextManager[None]: + return _dummy_context_manager() + + +@contextmanager +def _dummy_context_manager() -> Generator[None, None, None]: + yield diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/defaults.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/defaults.py index f771d4721c..347f8c6ad3 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/defaults.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/defaults.py @@ -1,63 +1,63 @@ -import sys -from typing import Optional, TextIO - -from prompt_toolkit.utils import is_windows - +import sys +from typing import Optional, TextIO + +from prompt_toolkit.utils import is_windows + from .base import DummyInput, Input, PipeInput - -__all__ = [ - "create_input", - "create_pipe_input", -] - - -def create_input( - stdin: Optional[TextIO] = None, always_prefer_tty: bool = False -) -> Input: - """ - Create the appropriate `Input` object for the current os/environment. - - :param always_prefer_tty: When set, if `sys.stdin` is connected to a Unix - `pipe`, check whether `sys.stdout` or `sys.stderr` are connected to a - pseudo terminal. If so, open the tty for reading instead of reading for - `sys.stdin`. (We can open `stdout` or `stderr` for reading, this is how - a `$PAGER` works.) - """ - if is_windows(): - from .win32 import Win32Input - + +__all__ = [ + "create_input", + "create_pipe_input", +] + + +def create_input( + stdin: Optional[TextIO] = None, always_prefer_tty: bool = False +) -> Input: + """ + Create the appropriate `Input` object for the current os/environment. + + :param always_prefer_tty: When set, if `sys.stdin` is connected to a Unix + `pipe`, check whether `sys.stdout` or `sys.stderr` are connected to a + pseudo terminal. If so, open the tty for reading instead of reading for + `sys.stdin`. (We can open `stdout` or `stderr` for reading, this is how + a `$PAGER` works.) + """ + if is_windows(): + from .win32 import Win32Input + # If `stdin` was assigned `None` (which happens with pythonw.exe), use # a `DummyInput`. This triggers `EOFError` in the application code. if stdin is None and sys.stdin is None: return DummyInput() - return Win32Input(stdin or sys.stdin) - else: - from .vt100 import Vt100Input - - # If no input TextIO is given, use stdin/stdout. - if stdin is None: - stdin = sys.stdin - - if always_prefer_tty: - for io in [sys.stdin, sys.stdout, sys.stderr]: - if io.isatty(): - stdin = io - break - - return Vt100Input(stdin) - - -def create_pipe_input() -> PipeInput: - """ - Create an input pipe. - This is mostly useful for unit testing. - """ - if is_windows(): - from .win32_pipe import Win32PipeInput - - return Win32PipeInput() - else: - from .posix_pipe import PosixPipeInput - - return PosixPipeInput() + return Win32Input(stdin or sys.stdin) + else: + from .vt100 import Vt100Input + + # If no input TextIO is given, use stdin/stdout. + if stdin is None: + stdin = sys.stdin + + if always_prefer_tty: + for io in [sys.stdin, sys.stdout, sys.stderr]: + if io.isatty(): + stdin = io + break + + return Vt100Input(stdin) + + +def create_pipe_input() -> PipeInput: + """ + Create an input pipe. + This is mostly useful for unit testing. + """ + if is_windows(): + from .win32_pipe import Win32PipeInput + + return Win32PipeInput() + else: + from .posix_pipe import PosixPipeInput + + return PosixPipeInput() diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/posix_pipe.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/posix_pipe.py index 61a798e869..de47c64933 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/posix_pipe.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/posix_pipe.py @@ -1,72 +1,72 @@ -import os -from typing import ContextManager, TextIO, cast - -from ..utils import DummyContext -from .base import PipeInput -from .vt100 import Vt100Input - -__all__ = [ - "PosixPipeInput", -] - - -class PosixPipeInput(Vt100Input, PipeInput): - """ - Input that is send through a pipe. - This is useful if we want to send the input programmatically into the - application. Mostly useful for unit testing. - - Usage:: - - input = PosixPipeInput() - input.send_text('inputdata') - """ - - _id = 0 - - def __init__(self, text: str = "") -> None: - self._r, self._w = os.pipe() - - class Stdin: - encoding = "utf-8" - - def isatty(stdin) -> bool: - return True - - def fileno(stdin) -> int: - return self._r - - super().__init__(cast(TextIO, Stdin())) - self.send_text(text) - - # Identifier for every PipeInput for the hash. - self.__class__._id += 1 - self._id = self.__class__._id - - def send_bytes(self, data: bytes) -> None: - os.write(self._w, data) - - def send_text(self, data: str) -> None: - "Send text to the input." - os.write(self._w, data.encode("utf-8")) - - def raw_mode(self) -> ContextManager[None]: - return DummyContext() - - def cooked_mode(self) -> ContextManager[None]: - return DummyContext() - - def close(self) -> None: - "Close pipe fds." - os.close(self._r) - os.close(self._w) - - # We should assign `None` to 'self._r` and 'self._w', - # The event loop still needs to know the the fileno for this input in order - # to properly remove it from the selectors. - - def typeahead_hash(self) -> str: - """ - This needs to be unique for every `PipeInput`. - """ - return "pipe-input-%s" % (self._id,) +import os +from typing import ContextManager, TextIO, cast + +from ..utils import DummyContext +from .base import PipeInput +from .vt100 import Vt100Input + +__all__ = [ + "PosixPipeInput", +] + + +class PosixPipeInput(Vt100Input, PipeInput): + """ + Input that is send through a pipe. + This is useful if we want to send the input programmatically into the + application. Mostly useful for unit testing. + + Usage:: + + input = PosixPipeInput() + input.send_text('inputdata') + """ + + _id = 0 + + def __init__(self, text: str = "") -> None: + self._r, self._w = os.pipe() + + class Stdin: + encoding = "utf-8" + + def isatty(stdin) -> bool: + return True + + def fileno(stdin) -> int: + return self._r + + super().__init__(cast(TextIO, Stdin())) + self.send_text(text) + + # Identifier for every PipeInput for the hash. + self.__class__._id += 1 + self._id = self.__class__._id + + def send_bytes(self, data: bytes) -> None: + os.write(self._w, data) + + def send_text(self, data: str) -> None: + "Send text to the input." + os.write(self._w, data.encode("utf-8")) + + def raw_mode(self) -> ContextManager[None]: + return DummyContext() + + def cooked_mode(self) -> ContextManager[None]: + return DummyContext() + + def close(self) -> None: + "Close pipe fds." + os.close(self._r) + os.close(self._w) + + # We should assign `None` to 'self._r` and 'self._w', + # The event loop still needs to know the the fileno for this input in order + # to properly remove it from the selectors. + + def typeahead_hash(self) -> str: + """ + This needs to be unique for every `PipeInput`. + """ + return "pipe-input-%s" % (self._id,) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/posix_utils.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/posix_utils.py index c0893239a3..f32f683f73 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/posix_utils.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/posix_utils.py @@ -1,95 +1,95 @@ -import os -import select -from codecs import getincrementaldecoder - -__all__ = [ - "PosixStdinReader", -] - - -class PosixStdinReader: - """ - Wrapper around stdin which reads (nonblocking) the next available 1024 - bytes and decodes it. - - Note that you can't be sure that the input file is closed if the ``read`` - function returns an empty string. When ``errors=ignore`` is passed, - ``read`` can return an empty string if all malformed input was replaced by - an empty string. (We can't block here and wait for more input.) So, because - of that, check the ``closed`` attribute, to be sure that the file has been - closed. - - :param stdin_fd: File descriptor from which we read. - :param errors: Can be 'ignore', 'strict' or 'replace'. - On Python3, this can be 'surrogateescape', which is the default. - - 'surrogateescape' is preferred, because this allows us to transfer - unrecognised bytes to the key bindings. Some terminals, like lxterminal - and Guake, use the 'Mxx' notation to send mouse events, where each 'x' - can be any possible byte. - """ - - # By default, we want to 'ignore' errors here. The input stream can be full - # of junk. One occurrence of this that I had was when using iTerm2 on OS X, - # with "Option as Meta" checked (You should choose "Option as +Esc".) - - def __init__( - self, stdin_fd: int, errors: str = "surrogateescape", encoding: str = "utf-8" - ) -> None: - self.stdin_fd = stdin_fd - self.errors = errors - - # Create incremental decoder for decoding stdin. - # We can not just do `os.read(stdin.fileno(), 1024).decode('utf-8')`, because - # it could be that we are in the middle of a utf-8 byte sequence. - self._stdin_decoder_cls = getincrementaldecoder(encoding) - self._stdin_decoder = self._stdin_decoder_cls(errors=errors) - - #: True when there is nothing anymore to read. - self.closed = False - - def read(self, count: int = 1024) -> str: - # By default we choose a rather small chunk size, because reading - # big amounts of input at once, causes the event loop to process - # all these key bindings also at once without going back to the - # loop. This will make the application feel unresponsive. - """ - Read the input and return it as a string. - - Return the text. Note that this can return an empty string, even when - the input stream was not yet closed. This means that something went - wrong during the decoding. - """ - if self.closed: - return "" - - # Check whether there is some input to read. `os.read` would block - # otherwise. - # (Actually, the event loop is responsible to make sure that this - # function is only called when there is something to read, but for some - # reason this happens in certain situations.) - try: - if not select.select([self.stdin_fd], [], [], 0)[0]: - return "" - except IOError: - # Happens for instance when the file descriptor was closed. - # (We had this in ptterm, where the FD became ready, a callback was - # scheduled, but in the meantime another callback closed it already.) - self.closed = True - - # Note: the following works better than wrapping `self.stdin` like - # `codecs.getreader('utf-8')(stdin)` and doing `read(1)`. - # Somehow that causes some latency when the escape - # character is pressed. (Especially on combination with the `select`.) - try: - data = os.read(self.stdin_fd, count) - - # Nothing more to read, stream is closed. - if data == b"": - self.closed = True - return "" - except OSError: - # In case of SIGWINCH - data = b"" - - return self._stdin_decoder.decode(data) +import os +import select +from codecs import getincrementaldecoder + +__all__ = [ + "PosixStdinReader", +] + + +class PosixStdinReader: + """ + Wrapper around stdin which reads (nonblocking) the next available 1024 + bytes and decodes it. + + Note that you can't be sure that the input file is closed if the ``read`` + function returns an empty string. When ``errors=ignore`` is passed, + ``read`` can return an empty string if all malformed input was replaced by + an empty string. (We can't block here and wait for more input.) So, because + of that, check the ``closed`` attribute, to be sure that the file has been + closed. + + :param stdin_fd: File descriptor from which we read. + :param errors: Can be 'ignore', 'strict' or 'replace'. + On Python3, this can be 'surrogateescape', which is the default. + + 'surrogateescape' is preferred, because this allows us to transfer + unrecognised bytes to the key bindings. Some terminals, like lxterminal + and Guake, use the 'Mxx' notation to send mouse events, where each 'x' + can be any possible byte. + """ + + # By default, we want to 'ignore' errors here. The input stream can be full + # of junk. One occurrence of this that I had was when using iTerm2 on OS X, + # with "Option as Meta" checked (You should choose "Option as +Esc".) + + def __init__( + self, stdin_fd: int, errors: str = "surrogateescape", encoding: str = "utf-8" + ) -> None: + self.stdin_fd = stdin_fd + self.errors = errors + + # Create incremental decoder for decoding stdin. + # We can not just do `os.read(stdin.fileno(), 1024).decode('utf-8')`, because + # it could be that we are in the middle of a utf-8 byte sequence. + self._stdin_decoder_cls = getincrementaldecoder(encoding) + self._stdin_decoder = self._stdin_decoder_cls(errors=errors) + + #: True when there is nothing anymore to read. + self.closed = False + + def read(self, count: int = 1024) -> str: + # By default we choose a rather small chunk size, because reading + # big amounts of input at once, causes the event loop to process + # all these key bindings also at once without going back to the + # loop. This will make the application feel unresponsive. + """ + Read the input and return it as a string. + + Return the text. Note that this can return an empty string, even when + the input stream was not yet closed. This means that something went + wrong during the decoding. + """ + if self.closed: + return "" + + # Check whether there is some input to read. `os.read` would block + # otherwise. + # (Actually, the event loop is responsible to make sure that this + # function is only called when there is something to read, but for some + # reason this happens in certain situations.) + try: + if not select.select([self.stdin_fd], [], [], 0)[0]: + return "" + except IOError: + # Happens for instance when the file descriptor was closed. + # (We had this in ptterm, where the FD became ready, a callback was + # scheduled, but in the meantime another callback closed it already.) + self.closed = True + + # Note: the following works better than wrapping `self.stdin` like + # `codecs.getreader('utf-8')(stdin)` and doing `read(1)`. + # Somehow that causes some latency when the escape + # character is pressed. (Especially on combination with the `select`.) + try: + data = os.read(self.stdin_fd, count) + + # Nothing more to read, stream is closed. + if data == b"": + self.closed = True + return "" + except OSError: + # In case of SIGWINCH + data = b"" + + return self._stdin_decoder.decode(data) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/typeahead.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/typeahead.py index df53afc374..a3d7866449 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/typeahead.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/typeahead.py @@ -1,76 +1,76 @@ -r""" -Store input key strokes if we did read more than was required. - -The input classes `Vt100Input` and `Win32Input` read the input text in chunks -of a few kilobytes. This means that if we read input from stdin, it could be -that we read a couple of lines (with newlines in between) at once. - -This creates a problem: potentially, we read too much from stdin. Sometimes -people paste several lines at once because they paste input in a REPL and -expect each input() call to process one line. Or they rely on type ahead -because the application can't keep up with the processing. - -However, we need to read input in bigger chunks. We need this mostly to support -pasting of larger chunks of text. We don't want everything to become -unresponsive because we: - - read one character; - - parse one character; - - call the key binding, which does a string operation with one character; - - and render the user interface. -Doing text operations on single characters is very inefficient in Python, so we -prefer to work on bigger chunks of text. This is why we have to read the input -in bigger chunks. - -Further, line buffering is also not an option, because it doesn't work well in -the architecture. We use lower level Posix APIs, that work better with the -event loop and so on. In fact, there is also nothing that defines that only \n -can accept the input, you could create a key binding for any key to accept the -input. - -To support type ahead, this module will store all the key strokes that were -read too early, so that they can be feed into to the next `prompt()` call or to -the next prompt_toolkit `Application`. -""" -from collections import defaultdict -from typing import Dict, List - -from ..key_binding import KeyPress -from .base import Input - -__all__ = [ - "store_typeahead", - "get_typeahead", - "clear_typeahead", -] - -_buffer: Dict[str, List[KeyPress]] = defaultdict(list) - - -def store_typeahead(input_obj: Input, key_presses: List[KeyPress]) -> None: - """ - Insert typeahead key presses for the given input. - """ - global _buffer - key = input_obj.typeahead_hash() - _buffer[key].extend(key_presses) - - -def get_typeahead(input_obj: Input) -> List[KeyPress]: - """ - Retrieve typeahead and reset the buffer for this input. - """ - global _buffer - - key = input_obj.typeahead_hash() - result = _buffer[key] - _buffer[key] = [] - return result - - -def clear_typeahead(input_obj: Input) -> None: - """ - Clear typeahead buffer. - """ - global _buffer - key = input_obj.typeahead_hash() - _buffer[key] = [] +r""" +Store input key strokes if we did read more than was required. + +The input classes `Vt100Input` and `Win32Input` read the input text in chunks +of a few kilobytes. This means that if we read input from stdin, it could be +that we read a couple of lines (with newlines in between) at once. + +This creates a problem: potentially, we read too much from stdin. Sometimes +people paste several lines at once because they paste input in a REPL and +expect each input() call to process one line. Or they rely on type ahead +because the application can't keep up with the processing. + +However, we need to read input in bigger chunks. We need this mostly to support +pasting of larger chunks of text. We don't want everything to become +unresponsive because we: + - read one character; + - parse one character; + - call the key binding, which does a string operation with one character; + - and render the user interface. +Doing text operations on single characters is very inefficient in Python, so we +prefer to work on bigger chunks of text. This is why we have to read the input +in bigger chunks. + +Further, line buffering is also not an option, because it doesn't work well in +the architecture. We use lower level Posix APIs, that work better with the +event loop and so on. In fact, there is also nothing that defines that only \n +can accept the input, you could create a key binding for any key to accept the +input. + +To support type ahead, this module will store all the key strokes that were +read too early, so that they can be feed into to the next `prompt()` call or to +the next prompt_toolkit `Application`. +""" +from collections import defaultdict +from typing import Dict, List + +from ..key_binding import KeyPress +from .base import Input + +__all__ = [ + "store_typeahead", + "get_typeahead", + "clear_typeahead", +] + +_buffer: Dict[str, List[KeyPress]] = defaultdict(list) + + +def store_typeahead(input_obj: Input, key_presses: List[KeyPress]) -> None: + """ + Insert typeahead key presses for the given input. + """ + global _buffer + key = input_obj.typeahead_hash() + _buffer[key].extend(key_presses) + + +def get_typeahead(input_obj: Input) -> List[KeyPress]: + """ + Retrieve typeahead and reset the buffer for this input. + """ + global _buffer + + key = input_obj.typeahead_hash() + result = _buffer[key] + _buffer[key] = [] + return result + + +def clear_typeahead(input_obj: Input) -> None: + """ + Clear typeahead buffer. + """ + global _buffer + key = input_obj.typeahead_hash() + _buffer[key] = [] diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/vt100.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/vt100.py index 1f32a7d58e..455cf8efd1 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/vt100.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/vt100.py @@ -1,174 +1,174 @@ -import contextlib -import io -import sys -import termios -import tty -from asyncio import AbstractEventLoop -from typing import ( - Callable, - ContextManager, - Dict, - Generator, - List, - Optional, - Set, - TextIO, - Tuple, - Union, -) - -from prompt_toolkit.eventloop import get_event_loop - -from ..key_binding import KeyPress -from .base import Input -from .posix_utils import PosixStdinReader -from .vt100_parser import Vt100Parser - -__all__ = [ - "Vt100Input", - "raw_mode", - "cooked_mode", -] - - -class Vt100Input(Input): - """ - Vt100 input for Posix systems. - (This uses a posix file descriptor that can be registered in the event loop.) - """ - - # For the error messages. Only display "Input is not a terminal" once per - # file descriptor. - _fds_not_a_terminal: Set[int] = set() - - def __init__(self, stdin: TextIO) -> None: - # Test whether the given input object has a file descriptor. - # (Idle reports stdin to be a TTY, but fileno() is not implemented.) - try: - # This should not raise, but can return 0. - stdin.fileno() - except io.UnsupportedOperation as e: - if "idlelib.run" in sys.modules: - raise io.UnsupportedOperation( - "Stdin is not a terminal. Running from Idle is not supported." - ) from e - else: - raise io.UnsupportedOperation("Stdin is not a terminal.") from e - - # Even when we have a file descriptor, it doesn't mean it's a TTY. - # Normally, this requires a real TTY device, but people instantiate - # this class often during unit tests as well. They use for instance - # pexpect to pipe data into an application. For convenience, we print - # an error message and go on. - isatty = stdin.isatty() - fd = stdin.fileno() - - if not isatty and fd not in Vt100Input._fds_not_a_terminal: - msg = "Warning: Input is not a terminal (fd=%r).\n" - sys.stderr.write(msg % fd) - sys.stderr.flush() - Vt100Input._fds_not_a_terminal.add(fd) - - # - self.stdin = stdin - - # Create a backup of the fileno(). We want this to work even if the - # underlying file is closed, so that `typeahead_hash()` keeps working. - self._fileno = stdin.fileno() - - self._buffer: List[KeyPress] = [] # Buffer to collect the Key objects. - self.stdin_reader = PosixStdinReader(self._fileno, encoding=stdin.encoding) - self.vt100_parser = Vt100Parser( - lambda key_press: self._buffer.append(key_press) - ) - - def attach(self, input_ready_callback: Callable[[], None]) -> ContextManager[None]: - """ - Return a context manager that makes this input active in the current - event loop. - """ - return _attached_input(self, input_ready_callback) - - def detach(self) -> ContextManager[None]: - """ - Return a context manager that makes sure that this input is not active - in the current event loop. - """ - return _detached_input(self) - - def read_keys(self) -> List[KeyPress]: - "Read list of KeyPress." - # Read text from stdin. - data = self.stdin_reader.read() - - # Pass it through our vt100 parser. - self.vt100_parser.feed(data) - - # Return result. - result = self._buffer - self._buffer = [] - return result - - def flush_keys(self) -> List[KeyPress]: - """ - Flush pending keys and return them. - (Used for flushing the 'escape' key.) - """ - # Flush all pending keys. (This is most important to flush the vt100 - # 'Escape' key early when nothing else follows.) - self.vt100_parser.flush() - - # Return result. - result = self._buffer - self._buffer = [] - return result - - @property - def closed(self) -> bool: - return self.stdin_reader.closed - - def raw_mode(self) -> ContextManager[None]: - return raw_mode(self.stdin.fileno()) - - def cooked_mode(self) -> ContextManager[None]: - return cooked_mode(self.stdin.fileno()) - - def fileno(self) -> int: - return self.stdin.fileno() - - def typeahead_hash(self) -> str: - return "fd-%s" % (self._fileno,) - - -_current_callbacks: Dict[ - Tuple[AbstractEventLoop, int], Optional[Callable[[], None]] -] = {} # (loop, fd) -> current callback - - -@contextlib.contextmanager -def _attached_input( - input: Vt100Input, callback: Callable[[], None] -) -> Generator[None, None, None]: - """ - Context manager that makes this input active in the current event loop. - - :param input: :class:`~prompt_toolkit.input.Input` object. - :param callback: Called when the input is ready to read. - """ - loop = get_event_loop() - fd = input.fileno() - previous = _current_callbacks.get((loop, fd)) - - def callback_wrapper() -> None: - """Wrapper around the callback that already removes the reader when - the input is closed. Otherwise, we keep continuously calling this - callback, until we leave the context manager (which can happen a bit - later). This fixes issues when piping /dev/null into a prompt_toolkit - application.""" - if input.closed: - loop.remove_reader(fd) - callback() - +import contextlib +import io +import sys +import termios +import tty +from asyncio import AbstractEventLoop +from typing import ( + Callable, + ContextManager, + Dict, + Generator, + List, + Optional, + Set, + TextIO, + Tuple, + Union, +) + +from prompt_toolkit.eventloop import get_event_loop + +from ..key_binding import KeyPress +from .base import Input +from .posix_utils import PosixStdinReader +from .vt100_parser import Vt100Parser + +__all__ = [ + "Vt100Input", + "raw_mode", + "cooked_mode", +] + + +class Vt100Input(Input): + """ + Vt100 input for Posix systems. + (This uses a posix file descriptor that can be registered in the event loop.) + """ + + # For the error messages. Only display "Input is not a terminal" once per + # file descriptor. + _fds_not_a_terminal: Set[int] = set() + + def __init__(self, stdin: TextIO) -> None: + # Test whether the given input object has a file descriptor. + # (Idle reports stdin to be a TTY, but fileno() is not implemented.) + try: + # This should not raise, but can return 0. + stdin.fileno() + except io.UnsupportedOperation as e: + if "idlelib.run" in sys.modules: + raise io.UnsupportedOperation( + "Stdin is not a terminal. Running from Idle is not supported." + ) from e + else: + raise io.UnsupportedOperation("Stdin is not a terminal.") from e + + # Even when we have a file descriptor, it doesn't mean it's a TTY. + # Normally, this requires a real TTY device, but people instantiate + # this class often during unit tests as well. They use for instance + # pexpect to pipe data into an application. For convenience, we print + # an error message and go on. + isatty = stdin.isatty() + fd = stdin.fileno() + + if not isatty and fd not in Vt100Input._fds_not_a_terminal: + msg = "Warning: Input is not a terminal (fd=%r).\n" + sys.stderr.write(msg % fd) + sys.stderr.flush() + Vt100Input._fds_not_a_terminal.add(fd) + + # + self.stdin = stdin + + # Create a backup of the fileno(). We want this to work even if the + # underlying file is closed, so that `typeahead_hash()` keeps working. + self._fileno = stdin.fileno() + + self._buffer: List[KeyPress] = [] # Buffer to collect the Key objects. + self.stdin_reader = PosixStdinReader(self._fileno, encoding=stdin.encoding) + self.vt100_parser = Vt100Parser( + lambda key_press: self._buffer.append(key_press) + ) + + def attach(self, input_ready_callback: Callable[[], None]) -> ContextManager[None]: + """ + Return a context manager that makes this input active in the current + event loop. + """ + return _attached_input(self, input_ready_callback) + + def detach(self) -> ContextManager[None]: + """ + Return a context manager that makes sure that this input is not active + in the current event loop. + """ + return _detached_input(self) + + def read_keys(self) -> List[KeyPress]: + "Read list of KeyPress." + # Read text from stdin. + data = self.stdin_reader.read() + + # Pass it through our vt100 parser. + self.vt100_parser.feed(data) + + # Return result. + result = self._buffer + self._buffer = [] + return result + + def flush_keys(self) -> List[KeyPress]: + """ + Flush pending keys and return them. + (Used for flushing the 'escape' key.) + """ + # Flush all pending keys. (This is most important to flush the vt100 + # 'Escape' key early when nothing else follows.) + self.vt100_parser.flush() + + # Return result. + result = self._buffer + self._buffer = [] + return result + + @property + def closed(self) -> bool: + return self.stdin_reader.closed + + def raw_mode(self) -> ContextManager[None]: + return raw_mode(self.stdin.fileno()) + + def cooked_mode(self) -> ContextManager[None]: + return cooked_mode(self.stdin.fileno()) + + def fileno(self) -> int: + return self.stdin.fileno() + + def typeahead_hash(self) -> str: + return "fd-%s" % (self._fileno,) + + +_current_callbacks: Dict[ + Tuple[AbstractEventLoop, int], Optional[Callable[[], None]] +] = {} # (loop, fd) -> current callback + + +@contextlib.contextmanager +def _attached_input( + input: Vt100Input, callback: Callable[[], None] +) -> Generator[None, None, None]: + """ + Context manager that makes this input active in the current event loop. + + :param input: :class:`~prompt_toolkit.input.Input` object. + :param callback: Called when the input is ready to read. + """ + loop = get_event_loop() + fd = input.fileno() + previous = _current_callbacks.get((loop, fd)) + + def callback_wrapper() -> None: + """Wrapper around the callback that already removes the reader when + the input is closed. Otherwise, we keep continuously calling this + callback, until we leave the context manager (which can happen a bit + later). This fixes issues when piping /dev/null into a prompt_toolkit + application.""" + if input.closed: + loop.remove_reader(fd) + callback() + try: loop.add_reader(fd, callback_wrapper) except PermissionError: @@ -181,137 +181,137 @@ def _attached_input( # To reproduce, do: `ptpython 0< /dev/null 1< /dev/null` raise EOFError - _current_callbacks[loop, fd] = callback - - try: - yield - finally: - loop.remove_reader(fd) - - if previous: - loop.add_reader(fd, previous) - _current_callbacks[loop, fd] = previous - else: - del _current_callbacks[loop, fd] - - -@contextlib.contextmanager -def _detached_input(input: Vt100Input) -> Generator[None, None, None]: - loop = get_event_loop() - fd = input.fileno() - previous = _current_callbacks.get((loop, fd)) - - if previous: - loop.remove_reader(fd) - _current_callbacks[loop, fd] = None - - try: - yield - finally: - if previous: - loop.add_reader(fd, previous) - _current_callbacks[loop, fd] = previous - - -class raw_mode: - """ - :: - - with raw_mode(stdin): - ''' the pseudo-terminal stdin is now used in raw mode ''' - - We ignore errors when executing `tcgetattr` fails. - """ - - # There are several reasons for ignoring errors: - # 1. To avoid the "Inappropriate ioctl for device" crash if somebody would - # execute this code (In a Python REPL, for instance): - # - # import os; f = open(os.devnull); os.dup2(f.fileno(), 0) - # - # The result is that the eventloop will stop correctly, because it has - # to logic to quit when stdin is closed. However, we should not fail at - # this point. See: - # https://github.com/jonathanslenders/python-prompt-toolkit/pull/393 - # https://github.com/jonathanslenders/python-prompt-toolkit/issues/392 - - # 2. Related, when stdin is an SSH pipe, and no full terminal was allocated. - # See: https://github.com/jonathanslenders/python-prompt-toolkit/pull/165 - def __init__(self, fileno: int) -> None: - self.fileno = fileno - self.attrs_before: Optional[List[Union[int, List[Union[bytes, int]]]]] - try: - self.attrs_before = termios.tcgetattr(fileno) - except termios.error: - # Ignore attribute errors. - self.attrs_before = None - - def __enter__(self) -> None: - # NOTE: On os X systems, using pty.setraw() fails. Therefor we are using this: - try: - newattr = termios.tcgetattr(self.fileno) - except termios.error: - pass - else: - newattr[tty.LFLAG] = self._patch_lflag(newattr[tty.LFLAG]) - newattr[tty.IFLAG] = self._patch_iflag(newattr[tty.IFLAG]) - - # VMIN defines the number of characters read at a time in - # non-canonical mode. It seems to default to 1 on Linux, but on - # Solaris and derived operating systems it defaults to 4. (This is - # because the VMIN slot is the same as the VEOF slot, which - # defaults to ASCII EOT = Ctrl-D = 4.) - newattr[tty.CC][termios.VMIN] = 1 - - termios.tcsetattr(self.fileno, termios.TCSANOW, newattr) - - @classmethod - def _patch_lflag(cls, attrs: int) -> int: - return attrs & ~(termios.ECHO | termios.ICANON | termios.IEXTEN | termios.ISIG) - - @classmethod - def _patch_iflag(cls, attrs: int) -> int: - return attrs & ~( - # Disable XON/XOFF flow control on output and input. - # (Don't capture Ctrl-S and Ctrl-Q.) - # Like executing: "stty -ixon." - termios.IXON - | termios.IXOFF - | - # Don't translate carriage return into newline on input. - termios.ICRNL - | termios.INLCR - | termios.IGNCR - ) - - def __exit__(self, *a: object) -> None: - if self.attrs_before is not None: - try: - termios.tcsetattr(self.fileno, termios.TCSANOW, self.attrs_before) - except termios.error: - pass - - # # Put the terminal in application mode. - # self._stdout.write('\x1b[?1h') - - -class cooked_mode(raw_mode): - """ - The opposite of ``raw_mode``, used when we need cooked mode inside a - `raw_mode` block. Used in `Application.run_in_terminal`.:: - - with cooked_mode(stdin): - ''' the pseudo-terminal stdin is now used in cooked mode. ''' - """ - - @classmethod - def _patch_lflag(cls, attrs: int) -> int: - return attrs | (termios.ECHO | termios.ICANON | termios.IEXTEN | termios.ISIG) - - @classmethod - def _patch_iflag(cls, attrs: int) -> int: - # Turn the ICRNL flag back on. (Without this, calling `input()` in - # run_in_terminal doesn't work and displays ^M instead. Ptpython - # evaluates commands using `run_in_terminal`, so it's important that - # they translate ^M back into ^J.) - return attrs | termios.ICRNL + _current_callbacks[loop, fd] = callback + + try: + yield + finally: + loop.remove_reader(fd) + + if previous: + loop.add_reader(fd, previous) + _current_callbacks[loop, fd] = previous + else: + del _current_callbacks[loop, fd] + + +@contextlib.contextmanager +def _detached_input(input: Vt100Input) -> Generator[None, None, None]: + loop = get_event_loop() + fd = input.fileno() + previous = _current_callbacks.get((loop, fd)) + + if previous: + loop.remove_reader(fd) + _current_callbacks[loop, fd] = None + + try: + yield + finally: + if previous: + loop.add_reader(fd, previous) + _current_callbacks[loop, fd] = previous + + +class raw_mode: + """ + :: + + with raw_mode(stdin): + ''' the pseudo-terminal stdin is now used in raw mode ''' + + We ignore errors when executing `tcgetattr` fails. + """ + + # There are several reasons for ignoring errors: + # 1. To avoid the "Inappropriate ioctl for device" crash if somebody would + # execute this code (In a Python REPL, for instance): + # + # import os; f = open(os.devnull); os.dup2(f.fileno(), 0) + # + # The result is that the eventloop will stop correctly, because it has + # to logic to quit when stdin is closed. However, we should not fail at + # this point. See: + # https://github.com/jonathanslenders/python-prompt-toolkit/pull/393 + # https://github.com/jonathanslenders/python-prompt-toolkit/issues/392 + + # 2. Related, when stdin is an SSH pipe, and no full terminal was allocated. + # See: https://github.com/jonathanslenders/python-prompt-toolkit/pull/165 + def __init__(self, fileno: int) -> None: + self.fileno = fileno + self.attrs_before: Optional[List[Union[int, List[Union[bytes, int]]]]] + try: + self.attrs_before = termios.tcgetattr(fileno) + except termios.error: + # Ignore attribute errors. + self.attrs_before = None + + def __enter__(self) -> None: + # NOTE: On os X systems, using pty.setraw() fails. Therefor we are using this: + try: + newattr = termios.tcgetattr(self.fileno) + except termios.error: + pass + else: + newattr[tty.LFLAG] = self._patch_lflag(newattr[tty.LFLAG]) + newattr[tty.IFLAG] = self._patch_iflag(newattr[tty.IFLAG]) + + # VMIN defines the number of characters read at a time in + # non-canonical mode. It seems to default to 1 on Linux, but on + # Solaris and derived operating systems it defaults to 4. (This is + # because the VMIN slot is the same as the VEOF slot, which + # defaults to ASCII EOT = Ctrl-D = 4.) + newattr[tty.CC][termios.VMIN] = 1 + + termios.tcsetattr(self.fileno, termios.TCSANOW, newattr) + + @classmethod + def _patch_lflag(cls, attrs: int) -> int: + return attrs & ~(termios.ECHO | termios.ICANON | termios.IEXTEN | termios.ISIG) + + @classmethod + def _patch_iflag(cls, attrs: int) -> int: + return attrs & ~( + # Disable XON/XOFF flow control on output and input. + # (Don't capture Ctrl-S and Ctrl-Q.) + # Like executing: "stty -ixon." + termios.IXON + | termios.IXOFF + | + # Don't translate carriage return into newline on input. + termios.ICRNL + | termios.INLCR + | termios.IGNCR + ) + + def __exit__(self, *a: object) -> None: + if self.attrs_before is not None: + try: + termios.tcsetattr(self.fileno, termios.TCSANOW, self.attrs_before) + except termios.error: + pass + + # # Put the terminal in application mode. + # self._stdout.write('\x1b[?1h') + + +class cooked_mode(raw_mode): + """ + The opposite of ``raw_mode``, used when we need cooked mode inside a + `raw_mode` block. Used in `Application.run_in_terminal`.:: + + with cooked_mode(stdin): + ''' the pseudo-terminal stdin is now used in cooked mode. ''' + """ + + @classmethod + def _patch_lflag(cls, attrs: int) -> int: + return attrs | (termios.ECHO | termios.ICANON | termios.IEXTEN | termios.ISIG) + + @classmethod + def _patch_iflag(cls, attrs: int) -> int: + # Turn the ICRNL flag back on. (Without this, calling `input()` in + # run_in_terminal doesn't work and displays ^M instead. Ptpython + # evaluates commands using `run_in_terminal`, so it's important that + # they translate ^M back into ^J.) + return attrs | termios.ICRNL diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/vt100_parser.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/vt100_parser.py index 09e4cc91bf..3ee1e14fdd 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/vt100_parser.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/vt100_parser.py @@ -1,247 +1,247 @@ -""" -Parser for VT100 input stream. -""" -import re -from typing import Callable, Dict, Generator, Tuple, Union - -from ..key_binding.key_processor import KeyPress -from ..keys import Keys -from .ansi_escape_sequences import ANSI_SEQUENCES - -__all__ = [ - "Vt100Parser", -] - - -# Regex matching any CPR response -# (Note that we use '\Z' instead of '$', because '$' could include a trailing -# newline.) -_cpr_response_re = re.compile("^" + re.escape("\x1b[") + r"\d+;\d+R\Z") - -# Mouse events: -# Typical: "Esc[MaB*" Urxvt: "Esc[96;14;13M" and for Xterm SGR: "Esc[<64;85;12M" -_mouse_event_re = re.compile("^" + re.escape("\x1b[") + r"(<?[\d;]+[mM]|M...)\Z") - -# Regex matching any valid prefix of a CPR response. -# (Note that it doesn't contain the last character, the 'R'. The prefix has to -# be shorter.) -_cpr_response_prefix_re = re.compile("^" + re.escape("\x1b[") + r"[\d;]*\Z") - -_mouse_event_prefix_re = re.compile("^" + re.escape("\x1b[") + r"(<?[\d;]*|M.{0,2})\Z") - - -class _Flush: - """Helper object to indicate flush operation to the parser.""" - - pass - - -class _IsPrefixOfLongerMatchCache(Dict[str, bool]): - """ - Dictionary that maps input sequences to a boolean indicating whether there is - any key that start with this characters. - """ - - def __missing__(self, prefix: str) -> bool: - # (hard coded) If this could be a prefix of a CPR response, return - # True. - if _cpr_response_prefix_re.match(prefix) or _mouse_event_prefix_re.match( - prefix - ): - result = True - else: - # If this could be a prefix of anything else, also return True. - result = any( - v - for k, v in ANSI_SEQUENCES.items() - if k.startswith(prefix) and k != prefix - ) - - self[prefix] = result - return result - - -_IS_PREFIX_OF_LONGER_MATCH_CACHE = _IsPrefixOfLongerMatchCache() - - -class Vt100Parser: - """ - Parser for VT100 input stream. - Data can be fed through the `feed` method and the given callback will be - called with KeyPress objects. - - :: - - def callback(key): - pass - i = Vt100Parser(callback) - i.feed('data\x01...') - - :attr feed_key_callback: Function that will be called when a key is parsed. - """ - - # Lookup table of ANSI escape sequences for a VT100 terminal - # Hint: in order to know what sequences your terminal writes to stdin, run - # "od -c" and start typing. - def __init__(self, feed_key_callback: Callable[[KeyPress], None]) -> None: - self.feed_key_callback = feed_key_callback - self.reset() - - def reset(self, request: bool = False) -> None: - self._in_bracketed_paste = False - self._start_parser() - - def _start_parser(self) -> None: - """ - Start the parser coroutine. - """ - self._input_parser = self._input_parser_generator() - self._input_parser.send(None) # type: ignore - - def _get_match(self, prefix: str) -> Union[None, Keys, Tuple[Keys, ...]]: - """ - Return the key (or keys) that maps to this prefix. - """ - # (hard coded) If we match a CPR response, return Keys.CPRResponse. - # (This one doesn't fit in the ANSI_SEQUENCES, because it contains - # integer variables.) - if _cpr_response_re.match(prefix): - return Keys.CPRResponse - - elif _mouse_event_re.match(prefix): - return Keys.Vt100MouseEvent - - # Otherwise, use the mappings. - try: - return ANSI_SEQUENCES[prefix] - except KeyError: - return None - - def _input_parser_generator(self) -> Generator[None, Union[str, _Flush], None]: - """ - Coroutine (state machine) for the input parser. - """ - prefix = "" - retry = False - flush = False - - while True: - flush = False - - if retry: - retry = False - else: - # Get next character. - c = yield - - if isinstance(c, _Flush): - flush = True - else: - prefix += c - - # If we have some data, check for matches. - if prefix: - is_prefix_of_longer_match = _IS_PREFIX_OF_LONGER_MATCH_CACHE[prefix] - match = self._get_match(prefix) - - # Exact matches found, call handlers.. - if (flush or not is_prefix_of_longer_match) and match: - self._call_handler(match, prefix) - prefix = "" - - # No exact match found. - elif (flush or not is_prefix_of_longer_match) and not match: - found = False - retry = True - - # Loop over the input, try the longest match first and - # shift. - for i in range(len(prefix), 0, -1): - match = self._get_match(prefix[:i]) - if match: - self._call_handler(match, prefix[:i]) - prefix = prefix[i:] - found = True - - if not found: - self._call_handler(prefix[0], prefix[0]) - prefix = prefix[1:] - - def _call_handler( - self, key: Union[str, Keys, Tuple[Keys, ...]], insert_text: str - ) -> None: - """ - Callback to handler. - """ - if isinstance(key, tuple): - # Received ANSI sequence that corresponds with multiple keys - # (probably alt+something). Handle keys individually, but only pass - # data payload to first KeyPress (so that we won't insert it - # multiple times). - for i, k in enumerate(key): - self._call_handler(k, insert_text if i == 0 else "") - else: - if key == Keys.BracketedPaste: - self._in_bracketed_paste = True - self._paste_buffer = "" - else: - self.feed_key_callback(KeyPress(key, insert_text)) - - def feed(self, data: str) -> None: - """ - Feed the input stream. - - :param data: Input string (unicode). - """ - # Handle bracketed paste. (We bypass the parser that matches all other - # key presses and keep reading input until we see the end mark.) - # This is much faster then parsing character by character. - if self._in_bracketed_paste: - self._paste_buffer += data - end_mark = "\x1b[201~" - - if end_mark in self._paste_buffer: - end_index = self._paste_buffer.index(end_mark) - - # Feed content to key bindings. - paste_content = self._paste_buffer[:end_index] - self.feed_key_callback(KeyPress(Keys.BracketedPaste, paste_content)) - - # Quit bracketed paste mode and handle remaining input. - self._in_bracketed_paste = False - remaining = self._paste_buffer[end_index + len(end_mark) :] - self._paste_buffer = "" - - self.feed(remaining) - - # Handle normal input character by character. - else: - for i, c in enumerate(data): - if self._in_bracketed_paste: - # Quit loop and process from this position when the parser - # entered bracketed paste. - self.feed(data[i:]) - break - else: - self._input_parser.send(c) - - def flush(self) -> None: - """ - Flush the buffer of the input stream. - - This will allow us to handle the escape key (or maybe meta) sooner. - The input received by the escape key is actually the same as the first - characters of e.g. Arrow-Up, so without knowing what follows the escape - sequence, we don't know whether escape has been pressed, or whether - it's something else. This flush function should be called after a - timeout, and processes everything that's still in the buffer as-is, so - without assuming any characters will follow. - """ - self._input_parser.send(_Flush()) - - def feed_and_flush(self, data: str) -> None: - """ - Wrapper around ``feed`` and ``flush``. - """ - self.feed(data) - self.flush() +""" +Parser for VT100 input stream. +""" +import re +from typing import Callable, Dict, Generator, Tuple, Union + +from ..key_binding.key_processor import KeyPress +from ..keys import Keys +from .ansi_escape_sequences import ANSI_SEQUENCES + +__all__ = [ + "Vt100Parser", +] + + +# Regex matching any CPR response +# (Note that we use '\Z' instead of '$', because '$' could include a trailing +# newline.) +_cpr_response_re = re.compile("^" + re.escape("\x1b[") + r"\d+;\d+R\Z") + +# Mouse events: +# Typical: "Esc[MaB*" Urxvt: "Esc[96;14;13M" and for Xterm SGR: "Esc[<64;85;12M" +_mouse_event_re = re.compile("^" + re.escape("\x1b[") + r"(<?[\d;]+[mM]|M...)\Z") + +# Regex matching any valid prefix of a CPR response. +# (Note that it doesn't contain the last character, the 'R'. The prefix has to +# be shorter.) +_cpr_response_prefix_re = re.compile("^" + re.escape("\x1b[") + r"[\d;]*\Z") + +_mouse_event_prefix_re = re.compile("^" + re.escape("\x1b[") + r"(<?[\d;]*|M.{0,2})\Z") + + +class _Flush: + """Helper object to indicate flush operation to the parser.""" + + pass + + +class _IsPrefixOfLongerMatchCache(Dict[str, bool]): + """ + Dictionary that maps input sequences to a boolean indicating whether there is + any key that start with this characters. + """ + + def __missing__(self, prefix: str) -> bool: + # (hard coded) If this could be a prefix of a CPR response, return + # True. + if _cpr_response_prefix_re.match(prefix) or _mouse_event_prefix_re.match( + prefix + ): + result = True + else: + # If this could be a prefix of anything else, also return True. + result = any( + v + for k, v in ANSI_SEQUENCES.items() + if k.startswith(prefix) and k != prefix + ) + + self[prefix] = result + return result + + +_IS_PREFIX_OF_LONGER_MATCH_CACHE = _IsPrefixOfLongerMatchCache() + + +class Vt100Parser: + """ + Parser for VT100 input stream. + Data can be fed through the `feed` method and the given callback will be + called with KeyPress objects. + + :: + + def callback(key): + pass + i = Vt100Parser(callback) + i.feed('data\x01...') + + :attr feed_key_callback: Function that will be called when a key is parsed. + """ + + # Lookup table of ANSI escape sequences for a VT100 terminal + # Hint: in order to know what sequences your terminal writes to stdin, run + # "od -c" and start typing. + def __init__(self, feed_key_callback: Callable[[KeyPress], None]) -> None: + self.feed_key_callback = feed_key_callback + self.reset() + + def reset(self, request: bool = False) -> None: + self._in_bracketed_paste = False + self._start_parser() + + def _start_parser(self) -> None: + """ + Start the parser coroutine. + """ + self._input_parser = self._input_parser_generator() + self._input_parser.send(None) # type: ignore + + def _get_match(self, prefix: str) -> Union[None, Keys, Tuple[Keys, ...]]: + """ + Return the key (or keys) that maps to this prefix. + """ + # (hard coded) If we match a CPR response, return Keys.CPRResponse. + # (This one doesn't fit in the ANSI_SEQUENCES, because it contains + # integer variables.) + if _cpr_response_re.match(prefix): + return Keys.CPRResponse + + elif _mouse_event_re.match(prefix): + return Keys.Vt100MouseEvent + + # Otherwise, use the mappings. + try: + return ANSI_SEQUENCES[prefix] + except KeyError: + return None + + def _input_parser_generator(self) -> Generator[None, Union[str, _Flush], None]: + """ + Coroutine (state machine) for the input parser. + """ + prefix = "" + retry = False + flush = False + + while True: + flush = False + + if retry: + retry = False + else: + # Get next character. + c = yield + + if isinstance(c, _Flush): + flush = True + else: + prefix += c + + # If we have some data, check for matches. + if prefix: + is_prefix_of_longer_match = _IS_PREFIX_OF_LONGER_MATCH_CACHE[prefix] + match = self._get_match(prefix) + + # Exact matches found, call handlers.. + if (flush or not is_prefix_of_longer_match) and match: + self._call_handler(match, prefix) + prefix = "" + + # No exact match found. + elif (flush or not is_prefix_of_longer_match) and not match: + found = False + retry = True + + # Loop over the input, try the longest match first and + # shift. + for i in range(len(prefix), 0, -1): + match = self._get_match(prefix[:i]) + if match: + self._call_handler(match, prefix[:i]) + prefix = prefix[i:] + found = True + + if not found: + self._call_handler(prefix[0], prefix[0]) + prefix = prefix[1:] + + def _call_handler( + self, key: Union[str, Keys, Tuple[Keys, ...]], insert_text: str + ) -> None: + """ + Callback to handler. + """ + if isinstance(key, tuple): + # Received ANSI sequence that corresponds with multiple keys + # (probably alt+something). Handle keys individually, but only pass + # data payload to first KeyPress (so that we won't insert it + # multiple times). + for i, k in enumerate(key): + self._call_handler(k, insert_text if i == 0 else "") + else: + if key == Keys.BracketedPaste: + self._in_bracketed_paste = True + self._paste_buffer = "" + else: + self.feed_key_callback(KeyPress(key, insert_text)) + + def feed(self, data: str) -> None: + """ + Feed the input stream. + + :param data: Input string (unicode). + """ + # Handle bracketed paste. (We bypass the parser that matches all other + # key presses and keep reading input until we see the end mark.) + # This is much faster then parsing character by character. + if self._in_bracketed_paste: + self._paste_buffer += data + end_mark = "\x1b[201~" + + if end_mark in self._paste_buffer: + end_index = self._paste_buffer.index(end_mark) + + # Feed content to key bindings. + paste_content = self._paste_buffer[:end_index] + self.feed_key_callback(KeyPress(Keys.BracketedPaste, paste_content)) + + # Quit bracketed paste mode and handle remaining input. + self._in_bracketed_paste = False + remaining = self._paste_buffer[end_index + len(end_mark) :] + self._paste_buffer = "" + + self.feed(remaining) + + # Handle normal input character by character. + else: + for i, c in enumerate(data): + if self._in_bracketed_paste: + # Quit loop and process from this position when the parser + # entered bracketed paste. + self.feed(data[i:]) + break + else: + self._input_parser.send(c) + + def flush(self) -> None: + """ + Flush the buffer of the input stream. + + This will allow us to handle the escape key (or maybe meta) sooner. + The input received by the escape key is actually the same as the first + characters of e.g. Arrow-Up, so without knowing what follows the escape + sequence, we don't know whether escape has been pressed, or whether + it's something else. This flush function should be called after a + timeout, and processes everything that's still in the buffer as-is, so + without assuming any characters will follow. + """ + self._input_parser.send(_Flush()) + + def feed_and_flush(self, data: str) -> None: + """ + Wrapper around ``feed`` and ``flush``. + """ + self.feed(data) + self.flush() diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/win32.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/win32.py index 437affaa5d..97699e19b2 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/win32.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/win32.py @@ -1,758 +1,758 @@ -import os -import sys -from abc import abstractmethod -from contextlib import contextmanager - -from prompt_toolkit.eventloop import get_event_loop - -from ..utils import SPHINX_AUTODOC_RUNNING - -# Do not import win32-specific stuff when generating documentation. -# Otherwise RTD would be unable to generate docs for this module. -if not SPHINX_AUTODOC_RUNNING: - import msvcrt - from ctypes import windll - -from ctypes import Array, pointer -from ctypes.wintypes import DWORD, HANDLE -from typing import ( - Callable, - ContextManager, - Dict, - Iterable, - Iterator, - List, - Optional, - TextIO, -) - -from prompt_toolkit.eventloop import run_in_executor_with_context -from prompt_toolkit.eventloop.win32 import create_win32_event, wait_for_handles -from prompt_toolkit.key_binding.key_processor import KeyPress -from prompt_toolkit.keys import Keys -from prompt_toolkit.mouse_events import MouseButton, MouseEventType -from prompt_toolkit.win32_types import ( - INPUT_RECORD, - KEY_EVENT_RECORD, - MOUSE_EVENT_RECORD, - STD_INPUT_HANDLE, - EventTypes, -) - -from .ansi_escape_sequences import REVERSE_ANSI_SEQUENCES -from .base import Input - -__all__ = [ - "Win32Input", - "ConsoleInputReader", - "raw_mode", - "cooked_mode", - "attach_win32_input", - "detach_win32_input", -] - -# Win32 Constants for MOUSE_EVENT_RECORD. -# See: https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str -FROM_LEFT_1ST_BUTTON_PRESSED = 0x1 -RIGHTMOST_BUTTON_PRESSED = 0x2 -MOUSE_MOVED = 0x0001 -MOUSE_WHEELED = 0x0004 - - -class _Win32InputBase(Input): - """ - Base class for `Win32Input` and `Win32PipeInput`. - """ - - def __init__(self) -> None: - self.win32_handles = _Win32Handles() - - @property - @abstractmethod - def handle(self) -> HANDLE: - pass - - -class Win32Input(_Win32InputBase): - """ - `Input` class that reads from the Windows console. - """ - - def __init__(self, stdin: Optional[TextIO] = None) -> None: - super().__init__() - self.console_input_reader = ConsoleInputReader() - - def attach(self, input_ready_callback: Callable[[], None]) -> ContextManager[None]: - """ - Return a context manager that makes this input active in the current - event loop. - """ - return attach_win32_input(self, input_ready_callback) - - def detach(self) -> ContextManager[None]: - """ - Return a context manager that makes sure that this input is not active - in the current event loop. - """ - return detach_win32_input(self) - - def read_keys(self) -> List[KeyPress]: - return list(self.console_input_reader.read()) - - def flush(self) -> None: - pass - - @property - def closed(self) -> bool: - return False - - def raw_mode(self) -> ContextManager[None]: - return raw_mode() - - def cooked_mode(self) -> ContextManager[None]: - return cooked_mode() - - def fileno(self) -> int: - # The windows console doesn't depend on the file handle, so - # this is not used for the event loop (which uses the - # handle instead). But it's used in `Application.run_system_command` - # which opens a subprocess with a given stdin/stdout. - return sys.stdin.fileno() - - def typeahead_hash(self) -> str: - return "win32-input" - - def close(self) -> None: - self.console_input_reader.close() - - @property - def handle(self) -> HANDLE: - return self.console_input_reader.handle - - -class ConsoleInputReader: - """ - :param recognize_paste: When True, try to discover paste actions and turn - the event into a BracketedPaste. - """ - - # Keys with character data. - mappings = { - b"\x1b": Keys.Escape, - b"\x00": Keys.ControlSpace, # Control-Space (Also for Ctrl-@) - b"\x01": Keys.ControlA, # Control-A (home) - b"\x02": Keys.ControlB, # Control-B (emacs cursor left) - b"\x03": Keys.ControlC, # Control-C (interrupt) - b"\x04": Keys.ControlD, # Control-D (exit) - b"\x05": Keys.ControlE, # Control-E (end) - b"\x06": Keys.ControlF, # Control-F (cursor forward) - b"\x07": Keys.ControlG, # Control-G - b"\x08": Keys.ControlH, # Control-H (8) (Identical to '\b') - b"\x09": Keys.ControlI, # Control-I (9) (Identical to '\t') - b"\x0a": Keys.ControlJ, # Control-J (10) (Identical to '\n') - b"\x0b": Keys.ControlK, # Control-K (delete until end of line; vertical tab) - b"\x0c": Keys.ControlL, # Control-L (clear; form feed) - b"\x0d": Keys.ControlM, # Control-M (enter) - b"\x0e": Keys.ControlN, # Control-N (14) (history forward) - b"\x0f": Keys.ControlO, # Control-O (15) - b"\x10": Keys.ControlP, # Control-P (16) (history back) - b"\x11": Keys.ControlQ, # Control-Q - b"\x12": Keys.ControlR, # Control-R (18) (reverse search) - b"\x13": Keys.ControlS, # Control-S (19) (forward search) - b"\x14": Keys.ControlT, # Control-T - b"\x15": Keys.ControlU, # Control-U - b"\x16": Keys.ControlV, # Control-V - b"\x17": Keys.ControlW, # Control-W - b"\x18": Keys.ControlX, # Control-X - b"\x19": Keys.ControlY, # Control-Y (25) - b"\x1a": Keys.ControlZ, # Control-Z - b"\x1c": Keys.ControlBackslash, # Both Control-\ and Ctrl-| - b"\x1d": Keys.ControlSquareClose, # Control-] - b"\x1e": Keys.ControlCircumflex, # Control-^ - b"\x1f": Keys.ControlUnderscore, # Control-underscore (Also for Ctrl-hyphen.) - b"\x7f": Keys.Backspace, # (127) Backspace (ASCII Delete.) - } - - # Keys that don't carry character data. - keycodes = { - # Home/End - 33: Keys.PageUp, - 34: Keys.PageDown, - 35: Keys.End, - 36: Keys.Home, - # Arrows - 37: Keys.Left, - 38: Keys.Up, - 39: Keys.Right, - 40: Keys.Down, - 45: Keys.Insert, - 46: Keys.Delete, - # F-keys. - 112: Keys.F1, - 113: Keys.F2, - 114: Keys.F3, - 115: Keys.F4, - 116: Keys.F5, - 117: Keys.F6, - 118: Keys.F7, - 119: Keys.F8, - 120: Keys.F9, - 121: Keys.F10, - 122: Keys.F11, - 123: Keys.F12, - } - - LEFT_ALT_PRESSED = 0x0002 - RIGHT_ALT_PRESSED = 0x0001 - SHIFT_PRESSED = 0x0010 - LEFT_CTRL_PRESSED = 0x0008 - RIGHT_CTRL_PRESSED = 0x0004 - - def __init__(self, recognize_paste: bool = True) -> None: - self._fdcon = None - self.recognize_paste = recognize_paste - - # When stdin is a tty, use that handle, otherwise, create a handle from - # CONIN$. - self.handle: HANDLE - if sys.stdin.isatty(): - self.handle = HANDLE(windll.kernel32.GetStdHandle(STD_INPUT_HANDLE)) - else: - self._fdcon = os.open("CONIN$", os.O_RDWR | os.O_BINARY) - self.handle = HANDLE(msvcrt.get_osfhandle(self._fdcon)) - - def close(self) -> None: - "Close fdcon." - if self._fdcon is not None: - os.close(self._fdcon) - - def read(self) -> Iterable[KeyPress]: - """ - Return a list of `KeyPress` instances. It won't return anything when - there was nothing to read. (This function doesn't block.) - - http://msdn.microsoft.com/en-us/library/windows/desktop/ms684961(v=vs.85).aspx - """ - max_count = 2048 # Max events to read at the same time. - - read = DWORD(0) - arrtype = INPUT_RECORD * max_count - input_records = arrtype() - - # Check whether there is some input to read. `ReadConsoleInputW` would - # block otherwise. - # (Actually, the event loop is responsible to make sure that this - # function is only called when there is something to read, but for some - # reason this happened in the asyncio_win32 loop, and it's better to be - # safe anyway.) - if not wait_for_handles([self.handle], timeout=0): - return - - # Get next batch of input event. - windll.kernel32.ReadConsoleInputW( - self.handle, pointer(input_records), max_count, pointer(read) - ) - - # First, get all the keys from the input buffer, in order to determine - # whether we should consider this a paste event or not. - all_keys = list(self._get_keys(read, input_records)) - - # Fill in 'data' for key presses. - all_keys = [self._insert_key_data(key) for key in all_keys] - - # Correct non-bmp characters that are passed as separate surrogate codes - all_keys = list(self._merge_paired_surrogates(all_keys)) - - if self.recognize_paste and self._is_paste(all_keys): - gen = iter(all_keys) - k: Optional[KeyPress] - - for k in gen: - # Pasting: if the current key consists of text or \n, turn it - # into a BracketedPaste. - data = [] +import os +import sys +from abc import abstractmethod +from contextlib import contextmanager + +from prompt_toolkit.eventloop import get_event_loop + +from ..utils import SPHINX_AUTODOC_RUNNING + +# Do not import win32-specific stuff when generating documentation. +# Otherwise RTD would be unable to generate docs for this module. +if not SPHINX_AUTODOC_RUNNING: + import msvcrt + from ctypes import windll + +from ctypes import Array, pointer +from ctypes.wintypes import DWORD, HANDLE +from typing import ( + Callable, + ContextManager, + Dict, + Iterable, + Iterator, + List, + Optional, + TextIO, +) + +from prompt_toolkit.eventloop import run_in_executor_with_context +from prompt_toolkit.eventloop.win32 import create_win32_event, wait_for_handles +from prompt_toolkit.key_binding.key_processor import KeyPress +from prompt_toolkit.keys import Keys +from prompt_toolkit.mouse_events import MouseButton, MouseEventType +from prompt_toolkit.win32_types import ( + INPUT_RECORD, + KEY_EVENT_RECORD, + MOUSE_EVENT_RECORD, + STD_INPUT_HANDLE, + EventTypes, +) + +from .ansi_escape_sequences import REVERSE_ANSI_SEQUENCES +from .base import Input + +__all__ = [ + "Win32Input", + "ConsoleInputReader", + "raw_mode", + "cooked_mode", + "attach_win32_input", + "detach_win32_input", +] + +# Win32 Constants for MOUSE_EVENT_RECORD. +# See: https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str +FROM_LEFT_1ST_BUTTON_PRESSED = 0x1 +RIGHTMOST_BUTTON_PRESSED = 0x2 +MOUSE_MOVED = 0x0001 +MOUSE_WHEELED = 0x0004 + + +class _Win32InputBase(Input): + """ + Base class for `Win32Input` and `Win32PipeInput`. + """ + + def __init__(self) -> None: + self.win32_handles = _Win32Handles() + + @property + @abstractmethod + def handle(self) -> HANDLE: + pass + + +class Win32Input(_Win32InputBase): + """ + `Input` class that reads from the Windows console. + """ + + def __init__(self, stdin: Optional[TextIO] = None) -> None: + super().__init__() + self.console_input_reader = ConsoleInputReader() + + def attach(self, input_ready_callback: Callable[[], None]) -> ContextManager[None]: + """ + Return a context manager that makes this input active in the current + event loop. + """ + return attach_win32_input(self, input_ready_callback) + + def detach(self) -> ContextManager[None]: + """ + Return a context manager that makes sure that this input is not active + in the current event loop. + """ + return detach_win32_input(self) + + def read_keys(self) -> List[KeyPress]: + return list(self.console_input_reader.read()) + + def flush(self) -> None: + pass + + @property + def closed(self) -> bool: + return False + + def raw_mode(self) -> ContextManager[None]: + return raw_mode() + + def cooked_mode(self) -> ContextManager[None]: + return cooked_mode() + + def fileno(self) -> int: + # The windows console doesn't depend on the file handle, so + # this is not used for the event loop (which uses the + # handle instead). But it's used in `Application.run_system_command` + # which opens a subprocess with a given stdin/stdout. + return sys.stdin.fileno() + + def typeahead_hash(self) -> str: + return "win32-input" + + def close(self) -> None: + self.console_input_reader.close() + + @property + def handle(self) -> HANDLE: + return self.console_input_reader.handle + + +class ConsoleInputReader: + """ + :param recognize_paste: When True, try to discover paste actions and turn + the event into a BracketedPaste. + """ + + # Keys with character data. + mappings = { + b"\x1b": Keys.Escape, + b"\x00": Keys.ControlSpace, # Control-Space (Also for Ctrl-@) + b"\x01": Keys.ControlA, # Control-A (home) + b"\x02": Keys.ControlB, # Control-B (emacs cursor left) + b"\x03": Keys.ControlC, # Control-C (interrupt) + b"\x04": Keys.ControlD, # Control-D (exit) + b"\x05": Keys.ControlE, # Control-E (end) + b"\x06": Keys.ControlF, # Control-F (cursor forward) + b"\x07": Keys.ControlG, # Control-G + b"\x08": Keys.ControlH, # Control-H (8) (Identical to '\b') + b"\x09": Keys.ControlI, # Control-I (9) (Identical to '\t') + b"\x0a": Keys.ControlJ, # Control-J (10) (Identical to '\n') + b"\x0b": Keys.ControlK, # Control-K (delete until end of line; vertical tab) + b"\x0c": Keys.ControlL, # Control-L (clear; form feed) + b"\x0d": Keys.ControlM, # Control-M (enter) + b"\x0e": Keys.ControlN, # Control-N (14) (history forward) + b"\x0f": Keys.ControlO, # Control-O (15) + b"\x10": Keys.ControlP, # Control-P (16) (history back) + b"\x11": Keys.ControlQ, # Control-Q + b"\x12": Keys.ControlR, # Control-R (18) (reverse search) + b"\x13": Keys.ControlS, # Control-S (19) (forward search) + b"\x14": Keys.ControlT, # Control-T + b"\x15": Keys.ControlU, # Control-U + b"\x16": Keys.ControlV, # Control-V + b"\x17": Keys.ControlW, # Control-W + b"\x18": Keys.ControlX, # Control-X + b"\x19": Keys.ControlY, # Control-Y (25) + b"\x1a": Keys.ControlZ, # Control-Z + b"\x1c": Keys.ControlBackslash, # Both Control-\ and Ctrl-| + b"\x1d": Keys.ControlSquareClose, # Control-] + b"\x1e": Keys.ControlCircumflex, # Control-^ + b"\x1f": Keys.ControlUnderscore, # Control-underscore (Also for Ctrl-hyphen.) + b"\x7f": Keys.Backspace, # (127) Backspace (ASCII Delete.) + } + + # Keys that don't carry character data. + keycodes = { + # Home/End + 33: Keys.PageUp, + 34: Keys.PageDown, + 35: Keys.End, + 36: Keys.Home, + # Arrows + 37: Keys.Left, + 38: Keys.Up, + 39: Keys.Right, + 40: Keys.Down, + 45: Keys.Insert, + 46: Keys.Delete, + # F-keys. + 112: Keys.F1, + 113: Keys.F2, + 114: Keys.F3, + 115: Keys.F4, + 116: Keys.F5, + 117: Keys.F6, + 118: Keys.F7, + 119: Keys.F8, + 120: Keys.F9, + 121: Keys.F10, + 122: Keys.F11, + 123: Keys.F12, + } + + LEFT_ALT_PRESSED = 0x0002 + RIGHT_ALT_PRESSED = 0x0001 + SHIFT_PRESSED = 0x0010 + LEFT_CTRL_PRESSED = 0x0008 + RIGHT_CTRL_PRESSED = 0x0004 + + def __init__(self, recognize_paste: bool = True) -> None: + self._fdcon = None + self.recognize_paste = recognize_paste + + # When stdin is a tty, use that handle, otherwise, create a handle from + # CONIN$. + self.handle: HANDLE + if sys.stdin.isatty(): + self.handle = HANDLE(windll.kernel32.GetStdHandle(STD_INPUT_HANDLE)) + else: + self._fdcon = os.open("CONIN$", os.O_RDWR | os.O_BINARY) + self.handle = HANDLE(msvcrt.get_osfhandle(self._fdcon)) + + def close(self) -> None: + "Close fdcon." + if self._fdcon is not None: + os.close(self._fdcon) + + def read(self) -> Iterable[KeyPress]: + """ + Return a list of `KeyPress` instances. It won't return anything when + there was nothing to read. (This function doesn't block.) + + http://msdn.microsoft.com/en-us/library/windows/desktop/ms684961(v=vs.85).aspx + """ + max_count = 2048 # Max events to read at the same time. + + read = DWORD(0) + arrtype = INPUT_RECORD * max_count + input_records = arrtype() + + # Check whether there is some input to read. `ReadConsoleInputW` would + # block otherwise. + # (Actually, the event loop is responsible to make sure that this + # function is only called when there is something to read, but for some + # reason this happened in the asyncio_win32 loop, and it's better to be + # safe anyway.) + if not wait_for_handles([self.handle], timeout=0): + return + + # Get next batch of input event. + windll.kernel32.ReadConsoleInputW( + self.handle, pointer(input_records), max_count, pointer(read) + ) + + # First, get all the keys from the input buffer, in order to determine + # whether we should consider this a paste event or not. + all_keys = list(self._get_keys(read, input_records)) + + # Fill in 'data' for key presses. + all_keys = [self._insert_key_data(key) for key in all_keys] + + # Correct non-bmp characters that are passed as separate surrogate codes + all_keys = list(self._merge_paired_surrogates(all_keys)) + + if self.recognize_paste and self._is_paste(all_keys): + gen = iter(all_keys) + k: Optional[KeyPress] + + for k in gen: + # Pasting: if the current key consists of text or \n, turn it + # into a BracketedPaste. + data = [] while k and ( not isinstance(k.key, Keys) or k.key in {Keys.ControlJ, Keys.ControlM} ): - data.append(k.data) - try: - k = next(gen) - except StopIteration: - k = None - - if data: - yield KeyPress(Keys.BracketedPaste, "".join(data)) - if k is not None: - yield k - else: - for k2 in all_keys: - yield k2 - - def _insert_key_data(self, key_press: KeyPress) -> KeyPress: - """ - Insert KeyPress data, for vt100 compatibility. - """ - if key_press.data: - return key_press - - if isinstance(key_press.key, Keys): - data = REVERSE_ANSI_SEQUENCES.get(key_press.key, "") - else: - data = "" - - return KeyPress(key_press.key, data) - - def _get_keys( - self, read: DWORD, input_records: "Array[INPUT_RECORD]" - ) -> Iterator[KeyPress]: - """ - Generator that yields `KeyPress` objects from the input records. - """ - for i in range(read.value): - ir = input_records[i] - - # Get the right EventType from the EVENT_RECORD. - # (For some reason the Windows console application 'cmder' - # [http://gooseberrycreative.com/cmder/] can return '0' for - # ir.EventType. -- Just ignore that.) - if ir.EventType in EventTypes: - ev = getattr(ir.Event, EventTypes[ir.EventType]) - - # Process if this is a key event. (We also have mouse, menu and - # focus events.) - if type(ev) == KEY_EVENT_RECORD and ev.KeyDown: - for key_press in self._event_to_key_presses(ev): - yield key_press - - elif type(ev) == MOUSE_EVENT_RECORD: - for key_press in self._handle_mouse(ev): - yield key_press - - @staticmethod - def _merge_paired_surrogates(key_presses: List[KeyPress]) -> Iterator[KeyPress]: - """ - Combines consecutive KeyPresses with high and low surrogates into - single characters - """ - buffered_high_surrogate = None - for key in key_presses: - is_text = not isinstance(key.key, Keys) - is_high_surrogate = is_text and "\uD800" <= key.key <= "\uDBFF" - is_low_surrogate = is_text and "\uDC00" <= key.key <= "\uDFFF" - - if buffered_high_surrogate: - if is_low_surrogate: - # convert high surrogate + low surrogate to single character - fullchar = ( - (buffered_high_surrogate.key + key.key) - .encode("utf-16-le", "surrogatepass") - .decode("utf-16-le") - ) - key = KeyPress(fullchar, fullchar) - else: - yield buffered_high_surrogate - buffered_high_surrogate = None - - if is_high_surrogate: - buffered_high_surrogate = key - else: - yield key - - if buffered_high_surrogate: - yield buffered_high_surrogate - - @staticmethod - def _is_paste(keys: List[KeyPress]) -> bool: - """ - Return `True` when we should consider this list of keys as a paste - event. Pasted text on windows will be turned into a - `Keys.BracketedPaste` event. (It's not 100% correct, but it is probably - the best possible way to detect pasting of text and handle that - correctly.) - """ - # Consider paste when it contains at least one newline and at least one - # other character. - text_count = 0 - newline_count = 0 - - for k in keys: - if not isinstance(k.key, Keys): - text_count += 1 - if k.key == Keys.ControlM: - newline_count += 1 - + data.append(k.data) + try: + k = next(gen) + except StopIteration: + k = None + + if data: + yield KeyPress(Keys.BracketedPaste, "".join(data)) + if k is not None: + yield k + else: + for k2 in all_keys: + yield k2 + + def _insert_key_data(self, key_press: KeyPress) -> KeyPress: + """ + Insert KeyPress data, for vt100 compatibility. + """ + if key_press.data: + return key_press + + if isinstance(key_press.key, Keys): + data = REVERSE_ANSI_SEQUENCES.get(key_press.key, "") + else: + data = "" + + return KeyPress(key_press.key, data) + + def _get_keys( + self, read: DWORD, input_records: "Array[INPUT_RECORD]" + ) -> Iterator[KeyPress]: + """ + Generator that yields `KeyPress` objects from the input records. + """ + for i in range(read.value): + ir = input_records[i] + + # Get the right EventType from the EVENT_RECORD. + # (For some reason the Windows console application 'cmder' + # [http://gooseberrycreative.com/cmder/] can return '0' for + # ir.EventType. -- Just ignore that.) + if ir.EventType in EventTypes: + ev = getattr(ir.Event, EventTypes[ir.EventType]) + + # Process if this is a key event. (We also have mouse, menu and + # focus events.) + if type(ev) == KEY_EVENT_RECORD and ev.KeyDown: + for key_press in self._event_to_key_presses(ev): + yield key_press + + elif type(ev) == MOUSE_EVENT_RECORD: + for key_press in self._handle_mouse(ev): + yield key_press + + @staticmethod + def _merge_paired_surrogates(key_presses: List[KeyPress]) -> Iterator[KeyPress]: + """ + Combines consecutive KeyPresses with high and low surrogates into + single characters + """ + buffered_high_surrogate = None + for key in key_presses: + is_text = not isinstance(key.key, Keys) + is_high_surrogate = is_text and "\uD800" <= key.key <= "\uDBFF" + is_low_surrogate = is_text and "\uDC00" <= key.key <= "\uDFFF" + + if buffered_high_surrogate: + if is_low_surrogate: + # convert high surrogate + low surrogate to single character + fullchar = ( + (buffered_high_surrogate.key + key.key) + .encode("utf-16-le", "surrogatepass") + .decode("utf-16-le") + ) + key = KeyPress(fullchar, fullchar) + else: + yield buffered_high_surrogate + buffered_high_surrogate = None + + if is_high_surrogate: + buffered_high_surrogate = key + else: + yield key + + if buffered_high_surrogate: + yield buffered_high_surrogate + + @staticmethod + def _is_paste(keys: List[KeyPress]) -> bool: + """ + Return `True` when we should consider this list of keys as a paste + event. Pasted text on windows will be turned into a + `Keys.BracketedPaste` event. (It's not 100% correct, but it is probably + the best possible way to detect pasting of text and handle that + correctly.) + """ + # Consider paste when it contains at least one newline and at least one + # other character. + text_count = 0 + newline_count = 0 + + for k in keys: + if not isinstance(k.key, Keys): + text_count += 1 + if k.key == Keys.ControlM: + newline_count += 1 + return newline_count >= 1 and text_count >= 1 - - def _event_to_key_presses(self, ev: KEY_EVENT_RECORD) -> List[KeyPress]: - """ - For this `KEY_EVENT_RECORD`, return a list of `KeyPress` instances. - """ - assert type(ev) == KEY_EVENT_RECORD and ev.KeyDown - - result: Optional[KeyPress] = None - - control_key_state = ev.ControlKeyState - u_char = ev.uChar.UnicodeChar - # Use surrogatepass because u_char may be an unmatched surrogate - ascii_char = u_char.encode("utf-8", "surrogatepass") - - # NOTE: We don't use `ev.uChar.AsciiChar`. That appears to be the - # unicode code point truncated to 1 byte. See also: - # https://github.com/ipython/ipython/issues/10004 - # https://github.com/jonathanslenders/python-prompt-toolkit/issues/389 - - if u_char == "\x00": - if ev.VirtualKeyCode in self.keycodes: - result = KeyPress(self.keycodes[ev.VirtualKeyCode], "") - else: - if ascii_char in self.mappings: - if self.mappings[ascii_char] == Keys.ControlJ: - u_char = ( - "\n" # Windows sends \n, turn into \r for unix compatibility. - ) - result = KeyPress(self.mappings[ascii_char], u_char) - else: - result = KeyPress(u_char, u_char) - - # First we handle Shift-Control-Arrow/Home/End (need to do this first) - if ( - ( - control_key_state & self.LEFT_CTRL_PRESSED - or control_key_state & self.RIGHT_CTRL_PRESSED - ) - and control_key_state & self.SHIFT_PRESSED - and result - ): - mapping: Dict[str, str] = { - Keys.Left: Keys.ControlShiftLeft, - Keys.Right: Keys.ControlShiftRight, - Keys.Up: Keys.ControlShiftUp, - Keys.Down: Keys.ControlShiftDown, - Keys.Home: Keys.ControlShiftHome, - Keys.End: Keys.ControlShiftEnd, - Keys.Insert: Keys.ControlShiftInsert, - Keys.PageUp: Keys.ControlShiftPageUp, - Keys.PageDown: Keys.ControlShiftPageDown, - } - result.key = mapping.get(result.key, result.key) - - # Correctly handle Control-Arrow/Home/End and Control-Insert/Delete keys. - if ( - control_key_state & self.LEFT_CTRL_PRESSED - or control_key_state & self.RIGHT_CTRL_PRESSED - ) and result: - mapping = { - Keys.Left: Keys.ControlLeft, - Keys.Right: Keys.ControlRight, - Keys.Up: Keys.ControlUp, - Keys.Down: Keys.ControlDown, - Keys.Home: Keys.ControlHome, - Keys.End: Keys.ControlEnd, - Keys.Insert: Keys.ControlInsert, - Keys.Delete: Keys.ControlDelete, - Keys.PageUp: Keys.ControlPageUp, - Keys.PageDown: Keys.ControlPageDown, - } - result.key = mapping.get(result.key, result.key) - - # Turn 'Tab' into 'BackTab' when shift was pressed. - # Also handle other shift-key combination - if control_key_state & self.SHIFT_PRESSED and result: - mapping = { - Keys.Tab: Keys.BackTab, - Keys.Left: Keys.ShiftLeft, - Keys.Right: Keys.ShiftRight, - Keys.Up: Keys.ShiftUp, - Keys.Down: Keys.ShiftDown, - Keys.Home: Keys.ShiftHome, - Keys.End: Keys.ShiftEnd, - Keys.Insert: Keys.ShiftInsert, - Keys.Delete: Keys.ShiftDelete, - Keys.PageUp: Keys.ShiftPageUp, - Keys.PageDown: Keys.ShiftPageDown, - } - result.key = mapping.get(result.key, result.key) - - # Turn 'Space' into 'ControlSpace' when control was pressed. - if ( - ( - control_key_state & self.LEFT_CTRL_PRESSED - or control_key_state & self.RIGHT_CTRL_PRESSED - ) - and result - and result.data == " " - ): - result = KeyPress(Keys.ControlSpace, " ") - - # Turn Control-Enter into META-Enter. (On a vt100 terminal, we cannot - # detect this combination. But it's really practical on Windows.) - if ( - ( - control_key_state & self.LEFT_CTRL_PRESSED - or control_key_state & self.RIGHT_CTRL_PRESSED - ) - and result - and result.key == Keys.ControlJ - ): - return [KeyPress(Keys.Escape, ""), result] - - # Return result. If alt was pressed, prefix the result with an - # 'Escape' key, just like unix VT100 terminals do. - - # NOTE: Only replace the left alt with escape. The right alt key often - # acts as altgr and is used in many non US keyboard layouts for - # typing some special characters, like a backslash. We don't want - # all backslashes to be prefixed with escape. (Esc-\ has a - # meaning in E-macs, for instance.) - if result: - meta_pressed = control_key_state & self.LEFT_ALT_PRESSED - - if meta_pressed: - return [KeyPress(Keys.Escape, ""), result] - else: - return [result] - - else: - return [] - - def _handle_mouse(self, ev: MOUSE_EVENT_RECORD) -> List[KeyPress]: - """ - Handle mouse events. Return a list of KeyPress instances. - """ - event_flags = ev.EventFlags - button_state = ev.ButtonState - - event_type: Optional[MouseEventType] = None - button: MouseButton = MouseButton.NONE - - # Scroll events. - if event_flags & MOUSE_WHEELED: - if button_state > 0: - event_type = MouseEventType.SCROLL_UP - else: - event_type = MouseEventType.SCROLL_DOWN - else: - # Handle button state for non-scroll events. - if button_state == FROM_LEFT_1ST_BUTTON_PRESSED: - button = MouseButton.LEFT - - elif button_state == RIGHTMOST_BUTTON_PRESSED: - button = MouseButton.RIGHT - - # Move events. - if event_flags & MOUSE_MOVED: - event_type = MouseEventType.MOUSE_MOVE - - # No key pressed anymore: mouse up. - if event_type is None: - if button_state > 0: - # Some button pressed. - event_type = MouseEventType.MOUSE_DOWN - else: - # No button pressed. - event_type = MouseEventType.MOUSE_UP - - data = ";".join( - [ - button.value, - event_type.value, - str(ev.MousePosition.X), - str(ev.MousePosition.Y), - ] - ) - return [KeyPress(Keys.WindowsMouseEvent, data)] - - -class _Win32Handles: - """ - Utility to keep track of which handles are connectod to which callbacks. - - `add_win32_handle` starts a tiny event loop in another thread which waits - for the Win32 handle to become ready. When this happens, the callback will - be called in the current asyncio event loop using `call_soon_threadsafe`. - - `remove_win32_handle` will stop this tiny event loop. - - NOTE: We use this technique, so that we don't have to use the - `ProactorEventLoop` on Windows and we can wait for things like stdin - in a `SelectorEventLoop`. This is important, because our inputhook - mechanism (used by IPython), only works with the `SelectorEventLoop`. - """ - - def __init__(self) -> None: - self._handle_callbacks: Dict[int, Callable[[], None]] = {} - - # Windows Events that are triggered when we have to stop watching this - # handle. - self._remove_events: Dict[int, HANDLE] = {} - - def add_win32_handle(self, handle: HANDLE, callback: Callable[[], None]) -> None: - """ - Add a Win32 handle to the event loop. - """ - handle_value = handle.value - - if handle_value is None: - raise ValueError("Invalid handle.") - - # Make sure to remove a previous registered handler first. - self.remove_win32_handle(handle) - - loop = get_event_loop() - self._handle_callbacks[handle_value] = callback - - # Create remove event. - remove_event = create_win32_event() - self._remove_events[handle_value] = remove_event - - # Add reader. - def ready() -> None: - # Tell the callback that input's ready. - try: - callback() - finally: - run_in_executor_with_context(wait, loop=loop) - - # Wait for the input to become ready. - # (Use an executor for this, the Windows asyncio event loop doesn't - # allow us to wait for handles like stdin.) - def wait() -> None: - # Wait until either the handle becomes ready, or the remove event - # has been set. - result = wait_for_handles([remove_event, handle]) - - if result is remove_event: - windll.kernel32.CloseHandle(remove_event) - return - else: - loop.call_soon_threadsafe(ready) - - run_in_executor_with_context(wait, loop=loop) - - def remove_win32_handle(self, handle: HANDLE) -> Optional[Callable[[], None]]: - """ - Remove a Win32 handle from the event loop. - Return either the registered handler or `None`. - """ - if handle.value is None: - return None # Ignore. - - # Trigger remove events, so that the reader knows to stop. - try: - event = self._remove_events.pop(handle.value) - except KeyError: - pass - else: - windll.kernel32.SetEvent(event) - - try: - return self._handle_callbacks.pop(handle.value) - except KeyError: - return None - - -@contextmanager -def attach_win32_input( - input: _Win32InputBase, callback: Callable[[], None] -) -> Iterator[None]: - """ - Context manager that makes this input active in the current event loop. - - :param input: :class:`~prompt_toolkit.input.Input` object. - :param input_ready_callback: Called when the input is ready to read. - """ - win32_handles = input.win32_handles - handle = input.handle - - if handle.value is None: - raise ValueError("Invalid handle.") - - # Add reader. - previous_callback = win32_handles.remove_win32_handle(handle) - win32_handles.add_win32_handle(handle, callback) - - try: - yield - finally: - win32_handles.remove_win32_handle(handle) - - if previous_callback: - win32_handles.add_win32_handle(handle, previous_callback) - - -@contextmanager -def detach_win32_input(input: _Win32InputBase) -> Iterator[None]: - win32_handles = input.win32_handles - handle = input.handle - - if handle.value is None: - raise ValueError("Invalid handle.") - - previous_callback = win32_handles.remove_win32_handle(handle) - - try: - yield - finally: - if previous_callback: - win32_handles.add_win32_handle(handle, previous_callback) - - -class raw_mode: - """ - :: - - with raw_mode(stdin): - ''' the windows terminal is now in 'raw' mode. ''' - - The ``fileno`` attribute is ignored. This is to be compatible with the - `raw_input` method of `.vt100_input`. - """ - - def __init__(self, fileno: Optional[int] = None) -> None: - self.handle = HANDLE(windll.kernel32.GetStdHandle(STD_INPUT_HANDLE)) - - def __enter__(self) -> None: - # Remember original mode. - original_mode = DWORD() - windll.kernel32.GetConsoleMode(self.handle, pointer(original_mode)) - self.original_mode = original_mode - - self._patch() - - def _patch(self) -> None: - # Set raw - ENABLE_ECHO_INPUT = 0x0004 - ENABLE_LINE_INPUT = 0x0002 - ENABLE_PROCESSED_INPUT = 0x0001 - - windll.kernel32.SetConsoleMode( - self.handle, - self.original_mode.value - & ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT), - ) - - def __exit__(self, *a: object) -> None: - # Restore original mode - windll.kernel32.SetConsoleMode(self.handle, self.original_mode) - - -class cooked_mode(raw_mode): - """ - :: - - with cooked_mode(stdin): - ''' The pseudo-terminal stdin is now used in cooked mode. ''' - """ - - def _patch(self) -> None: - # Set cooked. - ENABLE_ECHO_INPUT = 0x0004 - ENABLE_LINE_INPUT = 0x0002 - ENABLE_PROCESSED_INPUT = 0x0001 - - windll.kernel32.SetConsoleMode( - self.handle, - self.original_mode.value - | (ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT), - ) + + def _event_to_key_presses(self, ev: KEY_EVENT_RECORD) -> List[KeyPress]: + """ + For this `KEY_EVENT_RECORD`, return a list of `KeyPress` instances. + """ + assert type(ev) == KEY_EVENT_RECORD and ev.KeyDown + + result: Optional[KeyPress] = None + + control_key_state = ev.ControlKeyState + u_char = ev.uChar.UnicodeChar + # Use surrogatepass because u_char may be an unmatched surrogate + ascii_char = u_char.encode("utf-8", "surrogatepass") + + # NOTE: We don't use `ev.uChar.AsciiChar`. That appears to be the + # unicode code point truncated to 1 byte. See also: + # https://github.com/ipython/ipython/issues/10004 + # https://github.com/jonathanslenders/python-prompt-toolkit/issues/389 + + if u_char == "\x00": + if ev.VirtualKeyCode in self.keycodes: + result = KeyPress(self.keycodes[ev.VirtualKeyCode], "") + else: + if ascii_char in self.mappings: + if self.mappings[ascii_char] == Keys.ControlJ: + u_char = ( + "\n" # Windows sends \n, turn into \r for unix compatibility. + ) + result = KeyPress(self.mappings[ascii_char], u_char) + else: + result = KeyPress(u_char, u_char) + + # First we handle Shift-Control-Arrow/Home/End (need to do this first) + if ( + ( + control_key_state & self.LEFT_CTRL_PRESSED + or control_key_state & self.RIGHT_CTRL_PRESSED + ) + and control_key_state & self.SHIFT_PRESSED + and result + ): + mapping: Dict[str, str] = { + Keys.Left: Keys.ControlShiftLeft, + Keys.Right: Keys.ControlShiftRight, + Keys.Up: Keys.ControlShiftUp, + Keys.Down: Keys.ControlShiftDown, + Keys.Home: Keys.ControlShiftHome, + Keys.End: Keys.ControlShiftEnd, + Keys.Insert: Keys.ControlShiftInsert, + Keys.PageUp: Keys.ControlShiftPageUp, + Keys.PageDown: Keys.ControlShiftPageDown, + } + result.key = mapping.get(result.key, result.key) + + # Correctly handle Control-Arrow/Home/End and Control-Insert/Delete keys. + if ( + control_key_state & self.LEFT_CTRL_PRESSED + or control_key_state & self.RIGHT_CTRL_PRESSED + ) and result: + mapping = { + Keys.Left: Keys.ControlLeft, + Keys.Right: Keys.ControlRight, + Keys.Up: Keys.ControlUp, + Keys.Down: Keys.ControlDown, + Keys.Home: Keys.ControlHome, + Keys.End: Keys.ControlEnd, + Keys.Insert: Keys.ControlInsert, + Keys.Delete: Keys.ControlDelete, + Keys.PageUp: Keys.ControlPageUp, + Keys.PageDown: Keys.ControlPageDown, + } + result.key = mapping.get(result.key, result.key) + + # Turn 'Tab' into 'BackTab' when shift was pressed. + # Also handle other shift-key combination + if control_key_state & self.SHIFT_PRESSED and result: + mapping = { + Keys.Tab: Keys.BackTab, + Keys.Left: Keys.ShiftLeft, + Keys.Right: Keys.ShiftRight, + Keys.Up: Keys.ShiftUp, + Keys.Down: Keys.ShiftDown, + Keys.Home: Keys.ShiftHome, + Keys.End: Keys.ShiftEnd, + Keys.Insert: Keys.ShiftInsert, + Keys.Delete: Keys.ShiftDelete, + Keys.PageUp: Keys.ShiftPageUp, + Keys.PageDown: Keys.ShiftPageDown, + } + result.key = mapping.get(result.key, result.key) + + # Turn 'Space' into 'ControlSpace' when control was pressed. + if ( + ( + control_key_state & self.LEFT_CTRL_PRESSED + or control_key_state & self.RIGHT_CTRL_PRESSED + ) + and result + and result.data == " " + ): + result = KeyPress(Keys.ControlSpace, " ") + + # Turn Control-Enter into META-Enter. (On a vt100 terminal, we cannot + # detect this combination. But it's really practical on Windows.) + if ( + ( + control_key_state & self.LEFT_CTRL_PRESSED + or control_key_state & self.RIGHT_CTRL_PRESSED + ) + and result + and result.key == Keys.ControlJ + ): + return [KeyPress(Keys.Escape, ""), result] + + # Return result. If alt was pressed, prefix the result with an + # 'Escape' key, just like unix VT100 terminals do. + + # NOTE: Only replace the left alt with escape. The right alt key often + # acts as altgr and is used in many non US keyboard layouts for + # typing some special characters, like a backslash. We don't want + # all backslashes to be prefixed with escape. (Esc-\ has a + # meaning in E-macs, for instance.) + if result: + meta_pressed = control_key_state & self.LEFT_ALT_PRESSED + + if meta_pressed: + return [KeyPress(Keys.Escape, ""), result] + else: + return [result] + + else: + return [] + + def _handle_mouse(self, ev: MOUSE_EVENT_RECORD) -> List[KeyPress]: + """ + Handle mouse events. Return a list of KeyPress instances. + """ + event_flags = ev.EventFlags + button_state = ev.ButtonState + + event_type: Optional[MouseEventType] = None + button: MouseButton = MouseButton.NONE + + # Scroll events. + if event_flags & MOUSE_WHEELED: + if button_state > 0: + event_type = MouseEventType.SCROLL_UP + else: + event_type = MouseEventType.SCROLL_DOWN + else: + # Handle button state for non-scroll events. + if button_state == FROM_LEFT_1ST_BUTTON_PRESSED: + button = MouseButton.LEFT + + elif button_state == RIGHTMOST_BUTTON_PRESSED: + button = MouseButton.RIGHT + + # Move events. + if event_flags & MOUSE_MOVED: + event_type = MouseEventType.MOUSE_MOVE + + # No key pressed anymore: mouse up. + if event_type is None: + if button_state > 0: + # Some button pressed. + event_type = MouseEventType.MOUSE_DOWN + else: + # No button pressed. + event_type = MouseEventType.MOUSE_UP + + data = ";".join( + [ + button.value, + event_type.value, + str(ev.MousePosition.X), + str(ev.MousePosition.Y), + ] + ) + return [KeyPress(Keys.WindowsMouseEvent, data)] + + +class _Win32Handles: + """ + Utility to keep track of which handles are connectod to which callbacks. + + `add_win32_handle` starts a tiny event loop in another thread which waits + for the Win32 handle to become ready. When this happens, the callback will + be called in the current asyncio event loop using `call_soon_threadsafe`. + + `remove_win32_handle` will stop this tiny event loop. + + NOTE: We use this technique, so that we don't have to use the + `ProactorEventLoop` on Windows and we can wait for things like stdin + in a `SelectorEventLoop`. This is important, because our inputhook + mechanism (used by IPython), only works with the `SelectorEventLoop`. + """ + + def __init__(self) -> None: + self._handle_callbacks: Dict[int, Callable[[], None]] = {} + + # Windows Events that are triggered when we have to stop watching this + # handle. + self._remove_events: Dict[int, HANDLE] = {} + + def add_win32_handle(self, handle: HANDLE, callback: Callable[[], None]) -> None: + """ + Add a Win32 handle to the event loop. + """ + handle_value = handle.value + + if handle_value is None: + raise ValueError("Invalid handle.") + + # Make sure to remove a previous registered handler first. + self.remove_win32_handle(handle) + + loop = get_event_loop() + self._handle_callbacks[handle_value] = callback + + # Create remove event. + remove_event = create_win32_event() + self._remove_events[handle_value] = remove_event + + # Add reader. + def ready() -> None: + # Tell the callback that input's ready. + try: + callback() + finally: + run_in_executor_with_context(wait, loop=loop) + + # Wait for the input to become ready. + # (Use an executor for this, the Windows asyncio event loop doesn't + # allow us to wait for handles like stdin.) + def wait() -> None: + # Wait until either the handle becomes ready, or the remove event + # has been set. + result = wait_for_handles([remove_event, handle]) + + if result is remove_event: + windll.kernel32.CloseHandle(remove_event) + return + else: + loop.call_soon_threadsafe(ready) + + run_in_executor_with_context(wait, loop=loop) + + def remove_win32_handle(self, handle: HANDLE) -> Optional[Callable[[], None]]: + """ + Remove a Win32 handle from the event loop. + Return either the registered handler or `None`. + """ + if handle.value is None: + return None # Ignore. + + # Trigger remove events, so that the reader knows to stop. + try: + event = self._remove_events.pop(handle.value) + except KeyError: + pass + else: + windll.kernel32.SetEvent(event) + + try: + return self._handle_callbacks.pop(handle.value) + except KeyError: + return None + + +@contextmanager +def attach_win32_input( + input: _Win32InputBase, callback: Callable[[], None] +) -> Iterator[None]: + """ + Context manager that makes this input active in the current event loop. + + :param input: :class:`~prompt_toolkit.input.Input` object. + :param input_ready_callback: Called when the input is ready to read. + """ + win32_handles = input.win32_handles + handle = input.handle + + if handle.value is None: + raise ValueError("Invalid handle.") + + # Add reader. + previous_callback = win32_handles.remove_win32_handle(handle) + win32_handles.add_win32_handle(handle, callback) + + try: + yield + finally: + win32_handles.remove_win32_handle(handle) + + if previous_callback: + win32_handles.add_win32_handle(handle, previous_callback) + + +@contextmanager +def detach_win32_input(input: _Win32InputBase) -> Iterator[None]: + win32_handles = input.win32_handles + handle = input.handle + + if handle.value is None: + raise ValueError("Invalid handle.") + + previous_callback = win32_handles.remove_win32_handle(handle) + + try: + yield + finally: + if previous_callback: + win32_handles.add_win32_handle(handle, previous_callback) + + +class raw_mode: + """ + :: + + with raw_mode(stdin): + ''' the windows terminal is now in 'raw' mode. ''' + + The ``fileno`` attribute is ignored. This is to be compatible with the + `raw_input` method of `.vt100_input`. + """ + + def __init__(self, fileno: Optional[int] = None) -> None: + self.handle = HANDLE(windll.kernel32.GetStdHandle(STD_INPUT_HANDLE)) + + def __enter__(self) -> None: + # Remember original mode. + original_mode = DWORD() + windll.kernel32.GetConsoleMode(self.handle, pointer(original_mode)) + self.original_mode = original_mode + + self._patch() + + def _patch(self) -> None: + # Set raw + ENABLE_ECHO_INPUT = 0x0004 + ENABLE_LINE_INPUT = 0x0002 + ENABLE_PROCESSED_INPUT = 0x0001 + + windll.kernel32.SetConsoleMode( + self.handle, + self.original_mode.value + & ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT), + ) + + def __exit__(self, *a: object) -> None: + # Restore original mode + windll.kernel32.SetConsoleMode(self.handle, self.original_mode) + + +class cooked_mode(raw_mode): + """ + :: + + with cooked_mode(stdin): + ''' The pseudo-terminal stdin is now used in cooked mode. ''' + """ + + def _patch(self) -> None: + # Set cooked. + ENABLE_ECHO_INPUT = 0x0004 + ENABLE_LINE_INPUT = 0x0002 + ENABLE_PROCESSED_INPUT = 0x0001 + + windll.kernel32.SetConsoleMode( + self.handle, + self.original_mode.value + | (ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT), + ) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/win32_pipe.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/win32_pipe.py index 67cf6f0ee3..cdcf084de1 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/win32_pipe.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/input/win32_pipe.py @@ -1,135 +1,135 @@ -from ctypes import windll -from ctypes.wintypes import HANDLE -from typing import Callable, ContextManager, List - -from prompt_toolkit.eventloop.win32 import create_win32_event - -from ..key_binding import KeyPress -from ..utils import DummyContext -from .base import PipeInput -from .vt100_parser import Vt100Parser -from .win32 import _Win32InputBase, attach_win32_input, detach_win32_input - -__all__ = ["Win32PipeInput"] - - -class Win32PipeInput(_Win32InputBase, PipeInput): - """ - This is an input pipe that works on Windows. - Text or bytes can be feed into the pipe, and key strokes can be read from - the pipe. This is useful if we want to send the input programmatically into - the application. Mostly useful for unit testing. - - Notice that even though it's Windows, we use vt100 escape sequences over - the pipe. - - Usage:: - - input = Win32PipeInput() - input.send_text('inputdata') - """ - - _id = 0 - - def __init__(self) -> None: - super().__init__() - # Event (handle) for registering this input in the event loop. - # This event is set when there is data available to read from the pipe. - # Note: We use this approach instead of using a regular pipe, like - # returned from `os.pipe()`, because making such a regular pipe - # non-blocking is tricky and this works really well. - self._event = create_win32_event() - - self._closed = False - - # Parser for incoming keys. - self._buffer: List[KeyPress] = [] # Buffer to collect the Key objects. - self.vt100_parser = Vt100Parser(lambda key: self._buffer.append(key)) - - # Identifier for every PipeInput for the hash. - self.__class__._id += 1 - self._id = self.__class__._id - - @property - def closed(self) -> bool: - return self._closed - - def fileno(self) -> int: - """ - The windows pipe doesn't depend on the file handle. - """ - raise NotImplementedError - - @property - def handle(self) -> HANDLE: - "The handle used for registering this pipe in the event loop." - return self._event - - def attach(self, input_ready_callback: Callable[[], None]) -> ContextManager[None]: - """ - Return a context manager that makes this input active in the current - event loop. - """ - return attach_win32_input(self, input_ready_callback) - - def detach(self) -> ContextManager[None]: - """ - Return a context manager that makes sure that this input is not active - in the current event loop. - """ - return detach_win32_input(self) - - def read_keys(self) -> List[KeyPress]: - "Read list of KeyPress." - - # Return result. - result = self._buffer - self._buffer = [] - - # Reset event. - windll.kernel32.ResetEvent(self._event) - - return result - - def flush_keys(self) -> List[KeyPress]: - """ - Flush pending keys and return them. - (Used for flushing the 'escape' key.) - """ - # Flush all pending keys. (This is most important to flush the vt100 - # 'Escape' key early when nothing else follows.) - self.vt100_parser.flush() - - # Return result. - result = self._buffer - self._buffer = [] - return result - - def send_bytes(self, data: bytes) -> None: - "Send bytes to the input." - self.send_text(data.decode("utf-8", "ignore")) - - def send_text(self, text: str) -> None: - "Send text to the input." - # Pass it through our vt100 parser. - self.vt100_parser.feed(text) - - # Set event. - windll.kernel32.SetEvent(self._event) - - def raw_mode(self) -> ContextManager[None]: - return DummyContext() - - def cooked_mode(self) -> ContextManager[None]: - return DummyContext() - - def close(self) -> None: - "Close pipe handles." - windll.kernel32.CloseHandle(self._event) - self._closed = True - - def typeahead_hash(self) -> str: - """ - This needs to be unique for every `PipeInput`. - """ - return "pipe-input-%s" % (self._id,) +from ctypes import windll +from ctypes.wintypes import HANDLE +from typing import Callable, ContextManager, List + +from prompt_toolkit.eventloop.win32 import create_win32_event + +from ..key_binding import KeyPress +from ..utils import DummyContext +from .base import PipeInput +from .vt100_parser import Vt100Parser +from .win32 import _Win32InputBase, attach_win32_input, detach_win32_input + +__all__ = ["Win32PipeInput"] + + +class Win32PipeInput(_Win32InputBase, PipeInput): + """ + This is an input pipe that works on Windows. + Text or bytes can be feed into the pipe, and key strokes can be read from + the pipe. This is useful if we want to send the input programmatically into + the application. Mostly useful for unit testing. + + Notice that even though it's Windows, we use vt100 escape sequences over + the pipe. + + Usage:: + + input = Win32PipeInput() + input.send_text('inputdata') + """ + + _id = 0 + + def __init__(self) -> None: + super().__init__() + # Event (handle) for registering this input in the event loop. + # This event is set when there is data available to read from the pipe. + # Note: We use this approach instead of using a regular pipe, like + # returned from `os.pipe()`, because making such a regular pipe + # non-blocking is tricky and this works really well. + self._event = create_win32_event() + + self._closed = False + + # Parser for incoming keys. + self._buffer: List[KeyPress] = [] # Buffer to collect the Key objects. + self.vt100_parser = Vt100Parser(lambda key: self._buffer.append(key)) + + # Identifier for every PipeInput for the hash. + self.__class__._id += 1 + self._id = self.__class__._id + + @property + def closed(self) -> bool: + return self._closed + + def fileno(self) -> int: + """ + The windows pipe doesn't depend on the file handle. + """ + raise NotImplementedError + + @property + def handle(self) -> HANDLE: + "The handle used for registering this pipe in the event loop." + return self._event + + def attach(self, input_ready_callback: Callable[[], None]) -> ContextManager[None]: + """ + Return a context manager that makes this input active in the current + event loop. + """ + return attach_win32_input(self, input_ready_callback) + + def detach(self) -> ContextManager[None]: + """ + Return a context manager that makes sure that this input is not active + in the current event loop. + """ + return detach_win32_input(self) + + def read_keys(self) -> List[KeyPress]: + "Read list of KeyPress." + + # Return result. + result = self._buffer + self._buffer = [] + + # Reset event. + windll.kernel32.ResetEvent(self._event) + + return result + + def flush_keys(self) -> List[KeyPress]: + """ + Flush pending keys and return them. + (Used for flushing the 'escape' key.) + """ + # Flush all pending keys. (This is most important to flush the vt100 + # 'Escape' key early when nothing else follows.) + self.vt100_parser.flush() + + # Return result. + result = self._buffer + self._buffer = [] + return result + + def send_bytes(self, data: bytes) -> None: + "Send bytes to the input." + self.send_text(data.decode("utf-8", "ignore")) + + def send_text(self, text: str) -> None: + "Send text to the input." + # Pass it through our vt100 parser. + self.vt100_parser.feed(text) + + # Set event. + windll.kernel32.SetEvent(self._event) + + def raw_mode(self) -> ContextManager[None]: + return DummyContext() + + def cooked_mode(self) -> ContextManager[None]: + return DummyContext() + + def close(self) -> None: + "Close pipe handles." + windll.kernel32.CloseHandle(self._event) + self._closed = True + + def typeahead_hash(self) -> str: + """ + This needs to be unique for every `PipeInput`. + """ + return "pipe-input-%s" % (self._id,) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/__init__.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/__init__.py index 140afdc06e..be10536915 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/__init__.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/__init__.py @@ -1,20 +1,20 @@ -from .key_bindings import ( - ConditionalKeyBindings, - DynamicKeyBindings, - KeyBindings, - KeyBindingsBase, - merge_key_bindings, -) -from .key_processor import KeyPress, KeyPressEvent - -__all__ = [ - # key_bindings. - "ConditionalKeyBindings", - "DynamicKeyBindings", - "KeyBindings", - "KeyBindingsBase", - "merge_key_bindings", - # key_processor - "KeyPress", - "KeyPressEvent", -] +from .key_bindings import ( + ConditionalKeyBindings, + DynamicKeyBindings, + KeyBindings, + KeyBindingsBase, + merge_key_bindings, +) +from .key_processor import KeyPress, KeyPressEvent + +__all__ = [ + # key_bindings. + "ConditionalKeyBindings", + "DynamicKeyBindings", + "KeyBindings", + "KeyBindingsBase", + "merge_key_bindings", + # key_processor + "KeyPress", + "KeyPressEvent", +] diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/auto_suggest.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/auto_suggest.py index a45e5a8bef..c016c0688f 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/auto_suggest.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/auto_suggest.py @@ -1,63 +1,63 @@ -""" -Key bindings for auto suggestion (for fish-style auto suggestion). -""" -import re - -from prompt_toolkit.application.current import get_app -from prompt_toolkit.filters import Condition, emacs_mode -from prompt_toolkit.key_binding.key_bindings import KeyBindings -from prompt_toolkit.key_binding.key_processor import KeyPressEvent - -__all__ = [ - "load_auto_suggest_bindings", -] - -E = KeyPressEvent - - -def load_auto_suggest_bindings() -> KeyBindings: - """ - Key bindings for accepting auto suggestion text. - - (This has to come after the Vi bindings, because they also have an - implementation for the "right arrow", but we really want the suggestion - binding when a suggestion is available.) - """ - key_bindings = KeyBindings() - handle = key_bindings.add - - @Condition - def suggestion_available() -> bool: - app = get_app() - return ( - app.current_buffer.suggestion is not None - and len(app.current_buffer.suggestion.text) > 0 - and app.current_buffer.document.is_cursor_at_the_end - ) - - @handle("c-f", filter=suggestion_available) - @handle("c-e", filter=suggestion_available) - @handle("right", filter=suggestion_available) - def _accept(event: E) -> None: - """ - Accept suggestion. - """ - b = event.current_buffer - suggestion = b.suggestion - - if suggestion: - b.insert_text(suggestion.text) - - @handle("escape", "f", filter=suggestion_available & emacs_mode) - def _fill(event: E) -> None: - """ - Fill partial suggestion. - """ - b = event.current_buffer - suggestion = b.suggestion - - if suggestion: - t = re.split(r"(\S+\s+)", suggestion.text) - b.insert_text(next(x for x in t if x)) - - return key_bindings +""" +Key bindings for auto suggestion (for fish-style auto suggestion). +""" +import re + +from prompt_toolkit.application.current import get_app +from prompt_toolkit.filters import Condition, emacs_mode +from prompt_toolkit.key_binding.key_bindings import KeyBindings +from prompt_toolkit.key_binding.key_processor import KeyPressEvent + +__all__ = [ + "load_auto_suggest_bindings", +] + +E = KeyPressEvent + + +def load_auto_suggest_bindings() -> KeyBindings: + """ + Key bindings for accepting auto suggestion text. + + (This has to come after the Vi bindings, because they also have an + implementation for the "right arrow", but we really want the suggestion + binding when a suggestion is available.) + """ + key_bindings = KeyBindings() + handle = key_bindings.add + + @Condition + def suggestion_available() -> bool: + app = get_app() + return ( + app.current_buffer.suggestion is not None + and len(app.current_buffer.suggestion.text) > 0 + and app.current_buffer.document.is_cursor_at_the_end + ) + + @handle("c-f", filter=suggestion_available) + @handle("c-e", filter=suggestion_available) + @handle("right", filter=suggestion_available) + def _accept(event: E) -> None: + """ + Accept suggestion. + """ + b = event.current_buffer + suggestion = b.suggestion + + if suggestion: + b.insert_text(suggestion.text) + + @handle("escape", "f", filter=suggestion_available & emacs_mode) + def _fill(event: E) -> None: + """ + Fill partial suggestion. + """ + b = event.current_buffer + suggestion = b.suggestion + + if suggestion: + t = re.split(r"(\S+\s+)", suggestion.text) + b.insert_text(next(x for x in t if x)) + + return key_bindings diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/basic.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/basic.py index f108a32cdc..fc8f964359 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/basic.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/basic.py @@ -1,253 +1,253 @@ -# pylint: disable=function-redefined -from prompt_toolkit.application.current import get_app -from prompt_toolkit.filters import ( - Condition, - emacs_insert_mode, - has_selection, - in_paste_mode, - is_multiline, - vi_insert_mode, -) -from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent -from prompt_toolkit.keys import Keys - -from ..key_bindings import KeyBindings -from .named_commands import get_by_name - -__all__ = [ - "load_basic_bindings", -] - -E = KeyPressEvent - - -def if_no_repeat(event: E) -> bool: - """Callable that returns True when the previous event was delivered to - another handler.""" - return not event.is_repeat - - -def load_basic_bindings() -> KeyBindings: - key_bindings = KeyBindings() - insert_mode = vi_insert_mode | emacs_insert_mode - handle = key_bindings.add - - @handle("c-a") - @handle("c-b") - @handle("c-c") - @handle("c-d") - @handle("c-e") - @handle("c-f") - @handle("c-g") - @handle("c-h") - @handle("c-i") - @handle("c-j") - @handle("c-k") - @handle("c-l") - @handle("c-m") - @handle("c-n") - @handle("c-o") - @handle("c-p") - @handle("c-q") - @handle("c-r") - @handle("c-s") - @handle("c-t") - @handle("c-u") - @handle("c-v") - @handle("c-w") - @handle("c-x") - @handle("c-y") - @handle("c-z") - @handle("f1") - @handle("f2") - @handle("f3") - @handle("f4") - @handle("f5") - @handle("f6") - @handle("f7") - @handle("f8") - @handle("f9") - @handle("f10") - @handle("f11") - @handle("f12") - @handle("f13") - @handle("f14") - @handle("f15") - @handle("f16") - @handle("f17") - @handle("f18") - @handle("f19") - @handle("f20") - @handle("f21") - @handle("f22") - @handle("f23") - @handle("f24") - @handle("c-@") # Also c-space. - @handle("c-\\") - @handle("c-]") - @handle("c-^") - @handle("c-_") - @handle("backspace") - @handle("up") - @handle("down") - @handle("right") - @handle("left") - @handle("s-up") - @handle("s-down") - @handle("s-right") - @handle("s-left") - @handle("home") - @handle("end") - @handle("s-home") - @handle("s-end") - @handle("delete") - @handle("s-delete") - @handle("c-delete") - @handle("pageup") - @handle("pagedown") - @handle("s-tab") - @handle("tab") - @handle("c-s-left") - @handle("c-s-right") - @handle("c-s-home") - @handle("c-s-end") - @handle("c-left") - @handle("c-right") - @handle("c-up") - @handle("c-down") - @handle("c-home") - @handle("c-end") - @handle("insert") - @handle("s-insert") - @handle("c-insert") +# pylint: disable=function-redefined +from prompt_toolkit.application.current import get_app +from prompt_toolkit.filters import ( + Condition, + emacs_insert_mode, + has_selection, + in_paste_mode, + is_multiline, + vi_insert_mode, +) +from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent +from prompt_toolkit.keys import Keys + +from ..key_bindings import KeyBindings +from .named_commands import get_by_name + +__all__ = [ + "load_basic_bindings", +] + +E = KeyPressEvent + + +def if_no_repeat(event: E) -> bool: + """Callable that returns True when the previous event was delivered to + another handler.""" + return not event.is_repeat + + +def load_basic_bindings() -> KeyBindings: + key_bindings = KeyBindings() + insert_mode = vi_insert_mode | emacs_insert_mode + handle = key_bindings.add + + @handle("c-a") + @handle("c-b") + @handle("c-c") + @handle("c-d") + @handle("c-e") + @handle("c-f") + @handle("c-g") + @handle("c-h") + @handle("c-i") + @handle("c-j") + @handle("c-k") + @handle("c-l") + @handle("c-m") + @handle("c-n") + @handle("c-o") + @handle("c-p") + @handle("c-q") + @handle("c-r") + @handle("c-s") + @handle("c-t") + @handle("c-u") + @handle("c-v") + @handle("c-w") + @handle("c-x") + @handle("c-y") + @handle("c-z") + @handle("f1") + @handle("f2") + @handle("f3") + @handle("f4") + @handle("f5") + @handle("f6") + @handle("f7") + @handle("f8") + @handle("f9") + @handle("f10") + @handle("f11") + @handle("f12") + @handle("f13") + @handle("f14") + @handle("f15") + @handle("f16") + @handle("f17") + @handle("f18") + @handle("f19") + @handle("f20") + @handle("f21") + @handle("f22") + @handle("f23") + @handle("f24") + @handle("c-@") # Also c-space. + @handle("c-\\") + @handle("c-]") + @handle("c-^") + @handle("c-_") + @handle("backspace") + @handle("up") + @handle("down") + @handle("right") + @handle("left") + @handle("s-up") + @handle("s-down") + @handle("s-right") + @handle("s-left") + @handle("home") + @handle("end") + @handle("s-home") + @handle("s-end") + @handle("delete") + @handle("s-delete") + @handle("c-delete") + @handle("pageup") + @handle("pagedown") + @handle("s-tab") + @handle("tab") + @handle("c-s-left") + @handle("c-s-right") + @handle("c-s-home") + @handle("c-s-end") + @handle("c-left") + @handle("c-right") + @handle("c-up") + @handle("c-down") + @handle("c-home") + @handle("c-end") + @handle("insert") + @handle("s-insert") + @handle("c-insert") @handle("<sigint>") - @handle(Keys.Ignore) - def _ignore(event: E) -> None: - """ - First, for any of these keys, Don't do anything by default. Also don't - catch them in the 'Any' handler which will insert them as data. - - If people want to insert these characters as a literal, they can always - do by doing a quoted insert. (ControlQ in emacs mode, ControlV in Vi - mode.) - """ - pass - - # Readline-style bindings. - handle("home")(get_by_name("beginning-of-line")) - handle("end")(get_by_name("end-of-line")) - handle("left")(get_by_name("backward-char")) - handle("right")(get_by_name("forward-char")) - handle("c-up")(get_by_name("previous-history")) - handle("c-down")(get_by_name("next-history")) - handle("c-l")(get_by_name("clear-screen")) - - handle("c-k", filter=insert_mode)(get_by_name("kill-line")) - handle("c-u", filter=insert_mode)(get_by_name("unix-line-discard")) - handle("backspace", filter=insert_mode, save_before=if_no_repeat)( - get_by_name("backward-delete-char") - ) - handle("delete", filter=insert_mode, save_before=if_no_repeat)( - get_by_name("delete-char") - ) - handle("c-delete", filter=insert_mode, save_before=if_no_repeat)( - get_by_name("delete-char") - ) - handle(Keys.Any, filter=insert_mode, save_before=if_no_repeat)( - get_by_name("self-insert") - ) - handle("c-t", filter=insert_mode)(get_by_name("transpose-chars")) - handle("c-i", filter=insert_mode)(get_by_name("menu-complete")) - handle("s-tab", filter=insert_mode)(get_by_name("menu-complete-backward")) - - # Control-W should delete, using whitespace as separator, while M-Del - # should delete using [^a-zA-Z0-9] as a boundary. - handle("c-w", filter=insert_mode)(get_by_name("unix-word-rubout")) - - handle("pageup", filter=~has_selection)(get_by_name("previous-history")) - handle("pagedown", filter=~has_selection)(get_by_name("next-history")) - - # CTRL keys. - - @Condition - def has_text_before_cursor() -> bool: - return bool(get_app().current_buffer.text) - - handle("c-d", filter=has_text_before_cursor & insert_mode)( - get_by_name("delete-char") - ) - - @handle("enter", filter=insert_mode & is_multiline) - def _newline(event: E) -> None: - """ - Newline (in case of multiline input. - """ - event.current_buffer.newline(copy_margin=not in_paste_mode()) - - @handle("c-j") - def _newline2(event: E) -> None: - r""" - By default, handle \n as if it were a \r (enter). - (It appears that some terminals send \n instead of \r when pressing - enter. - at least the Linux subsystem for Windows.) - """ - event.key_processor.feed(KeyPress(Keys.ControlM, "\r"), first=True) - - # Delete the word before the cursor. - - @handle("up") - def _go_up(event: E) -> None: - event.current_buffer.auto_up(count=event.arg) - - @handle("down") - def _go_down(event: E) -> None: - event.current_buffer.auto_down(count=event.arg) - - @handle("delete", filter=has_selection) - def _cut(event: E) -> None: - data = event.current_buffer.cut_selection() - event.app.clipboard.set_data(data) - - # Global bindings. - - @handle("c-z") - def _insert_ctrl_z(event: E) -> None: - """ - By default, control-Z should literally insert Ctrl-Z. - (Ansi Ctrl-Z, code 26 in MSDOS means End-Of-File. - In a Python REPL for instance, it's possible to type - Control-Z followed by enter to quit.) - - When the system bindings are loaded and suspend-to-background is - supported, that will override this binding. - """ - event.current_buffer.insert_text(event.data) - - @handle(Keys.BracketedPaste) - def _paste(event: E) -> None: - """ - Pasting from clipboard. - """ - data = event.data - - # Be sure to use \n as line ending. - # Some terminals (Like iTerm2) seem to paste \r\n line endings in a - # bracketed paste. See: https://github.com/ipython/ipython/issues/9737 - data = data.replace("\r\n", "\n") - data = data.replace("\r", "\n") - - event.current_buffer.insert_text(data) - - @Condition - def in_quoted_insert() -> bool: - return get_app().quoted_insert - - @handle(Keys.Any, filter=in_quoted_insert, eager=True) - def _insert_text(event: E) -> None: - """ - Handle quoted insert. - """ - event.current_buffer.insert_text(event.data, overwrite=False) - event.app.quoted_insert = False - - return key_bindings + @handle(Keys.Ignore) + def _ignore(event: E) -> None: + """ + First, for any of these keys, Don't do anything by default. Also don't + catch them in the 'Any' handler which will insert them as data. + + If people want to insert these characters as a literal, they can always + do by doing a quoted insert. (ControlQ in emacs mode, ControlV in Vi + mode.) + """ + pass + + # Readline-style bindings. + handle("home")(get_by_name("beginning-of-line")) + handle("end")(get_by_name("end-of-line")) + handle("left")(get_by_name("backward-char")) + handle("right")(get_by_name("forward-char")) + handle("c-up")(get_by_name("previous-history")) + handle("c-down")(get_by_name("next-history")) + handle("c-l")(get_by_name("clear-screen")) + + handle("c-k", filter=insert_mode)(get_by_name("kill-line")) + handle("c-u", filter=insert_mode)(get_by_name("unix-line-discard")) + handle("backspace", filter=insert_mode, save_before=if_no_repeat)( + get_by_name("backward-delete-char") + ) + handle("delete", filter=insert_mode, save_before=if_no_repeat)( + get_by_name("delete-char") + ) + handle("c-delete", filter=insert_mode, save_before=if_no_repeat)( + get_by_name("delete-char") + ) + handle(Keys.Any, filter=insert_mode, save_before=if_no_repeat)( + get_by_name("self-insert") + ) + handle("c-t", filter=insert_mode)(get_by_name("transpose-chars")) + handle("c-i", filter=insert_mode)(get_by_name("menu-complete")) + handle("s-tab", filter=insert_mode)(get_by_name("menu-complete-backward")) + + # Control-W should delete, using whitespace as separator, while M-Del + # should delete using [^a-zA-Z0-9] as a boundary. + handle("c-w", filter=insert_mode)(get_by_name("unix-word-rubout")) + + handle("pageup", filter=~has_selection)(get_by_name("previous-history")) + handle("pagedown", filter=~has_selection)(get_by_name("next-history")) + + # CTRL keys. + + @Condition + def has_text_before_cursor() -> bool: + return bool(get_app().current_buffer.text) + + handle("c-d", filter=has_text_before_cursor & insert_mode)( + get_by_name("delete-char") + ) + + @handle("enter", filter=insert_mode & is_multiline) + def _newline(event: E) -> None: + """ + Newline (in case of multiline input. + """ + event.current_buffer.newline(copy_margin=not in_paste_mode()) + + @handle("c-j") + def _newline2(event: E) -> None: + r""" + By default, handle \n as if it were a \r (enter). + (It appears that some terminals send \n instead of \r when pressing + enter. - at least the Linux subsystem for Windows.) + """ + event.key_processor.feed(KeyPress(Keys.ControlM, "\r"), first=True) + + # Delete the word before the cursor. + + @handle("up") + def _go_up(event: E) -> None: + event.current_buffer.auto_up(count=event.arg) + + @handle("down") + def _go_down(event: E) -> None: + event.current_buffer.auto_down(count=event.arg) + + @handle("delete", filter=has_selection) + def _cut(event: E) -> None: + data = event.current_buffer.cut_selection() + event.app.clipboard.set_data(data) + + # Global bindings. + + @handle("c-z") + def _insert_ctrl_z(event: E) -> None: + """ + By default, control-Z should literally insert Ctrl-Z. + (Ansi Ctrl-Z, code 26 in MSDOS means End-Of-File. + In a Python REPL for instance, it's possible to type + Control-Z followed by enter to quit.) + + When the system bindings are loaded and suspend-to-background is + supported, that will override this binding. + """ + event.current_buffer.insert_text(event.data) + + @handle(Keys.BracketedPaste) + def _paste(event: E) -> None: + """ + Pasting from clipboard. + """ + data = event.data + + # Be sure to use \n as line ending. + # Some terminals (Like iTerm2) seem to paste \r\n line endings in a + # bracketed paste. See: https://github.com/ipython/ipython/issues/9737 + data = data.replace("\r\n", "\n") + data = data.replace("\r", "\n") + + event.current_buffer.insert_text(data) + + @Condition + def in_quoted_insert() -> bool: + return get_app().quoted_insert + + @handle(Keys.Any, filter=in_quoted_insert, eager=True) + def _insert_text(event: E) -> None: + """ + Handle quoted insert. + """ + event.current_buffer.insert_text(event.data, overwrite=False) + event.app.quoted_insert = False + + return key_bindings diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/completion.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/completion.py index b6cd7e59f7..e52edf87ff 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/completion.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/completion.py @@ -1,203 +1,203 @@ -""" -Key binding handlers for displaying completions. -""" -import asyncio -import math -from typing import TYPE_CHECKING, List - -from prompt_toolkit.application.run_in_terminal import in_terminal -from prompt_toolkit.completion import ( - CompleteEvent, - Completion, - get_common_complete_suffix, -) -from prompt_toolkit.formatted_text import StyleAndTextTuples -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.utils import get_cwidth - -if TYPE_CHECKING: - from prompt_toolkit.application import Application - from prompt_toolkit.shortcuts import PromptSession - -__all__ = [ - "generate_completions", - "display_completions_like_readline", -] - -E = KeyPressEvent - - -def generate_completions(event: E) -> None: - r""" - Tab-completion: where the first tab completes the common suffix and the - second tab lists all the completions. - """ - b = event.current_buffer - - # When already navigating through completions, select the next one. - if b.complete_state: - b.complete_next() - else: - b.start_completion(insert_common_part=True) - - -def display_completions_like_readline(event: E) -> None: - """ - Key binding handler for readline-style tab completion. - This is meant to be as similar as possible to the way how readline displays - completions. - - Generate the completions immediately (blocking) and display them above the - prompt in columns. - - Usage:: - - # Call this handler when 'Tab' has been pressed. - key_bindings.add(Keys.ControlI)(display_completions_like_readline) - """ - # Request completions. - b = event.current_buffer - if b.completer is None: - return - complete_event = CompleteEvent(completion_requested=True) - completions = list(b.completer.get_completions(b.document, complete_event)) - - # Calculate the common suffix. - common_suffix = get_common_complete_suffix(b.document, completions) - - # One completion: insert it. - if len(completions) == 1: - b.delete_before_cursor(-completions[0].start_position) - b.insert_text(completions[0].text) - # Multiple completions with common part. - elif common_suffix: - b.insert_text(common_suffix) - # Otherwise: display all completions. - elif completions: - _display_completions_like_readline(event.app, completions) - - -def _display_completions_like_readline( - app: "Application[object]", completions: List[Completion] -) -> "asyncio.Task[None]": - """ - Display the list of completions in columns above the prompt. - This will ask for a confirmation if there are too many completions to fit - on a single page and provide a paginator to walk through them. - """ - from prompt_toolkit.formatted_text import to_formatted_text - from prompt_toolkit.shortcuts.prompt import create_confirm_session - - # Get terminal dimensions. - term_size = app.output.get_size() - term_width = term_size.columns - term_height = term_size.rows - - # Calculate amount of required columns/rows for displaying the - # completions. (Keep in mind that completions are displayed - # alphabetically column-wise.) - max_compl_width = min( - term_width, max(get_cwidth(c.display_text) for c in completions) + 1 - ) - column_count = max(1, term_width // max_compl_width) - completions_per_page = column_count * (term_height - 1) - page_count = int(math.ceil(len(completions) / float(completions_per_page))) - # Note: math.ceil can return float on Python2. - - def display(page: int) -> None: - # Display completions. - page_completions = completions[ - page * completions_per_page : (page + 1) * completions_per_page - ] - - page_row_count = int(math.ceil(len(page_completions) / float(column_count))) - page_columns = [ - page_completions[i * page_row_count : (i + 1) * page_row_count] - for i in range(column_count) - ] - - result: StyleAndTextTuples = [] - - for r in range(page_row_count): - for c in range(column_count): - try: - completion = page_columns[c][r] - style = "class:readline-like-completions.completion " + ( - completion.style or "" - ) - - result.extend(to_formatted_text(completion.display, style=style)) - - # Add padding. - padding = max_compl_width - get_cwidth(completion.display_text) - result.append((completion.style, " " * padding)) - except IndexError: - pass - result.append(("", "\n")) - - app.print_text(to_formatted_text(result, "class:readline-like-completions")) - - # User interaction through an application generator function. - async def run_compl() -> None: - "Coroutine." - async with in_terminal(render_cli_done=True): - if len(completions) > completions_per_page: - # Ask confirmation if it doesn't fit on the screen. - confirm = await create_confirm_session( - "Display all {} possibilities?".format(len(completions)), - ).prompt_async() - - if confirm: - # Display pages. - for page in range(page_count): - display(page) - - if page != page_count - 1: - # Display --MORE-- and go to the next page. - show_more = await _create_more_session( - "--MORE--" - ).prompt_async() - - if not show_more: - return - else: - app.output.flush() - else: - # Display all completions. - display(0) - - return app.create_background_task(run_compl()) - - -def _create_more_session(message: str = "--MORE--") -> "PromptSession[bool]": - """ - Create a `PromptSession` object for displaying the "--MORE--". - """ - from prompt_toolkit.shortcuts import PromptSession - - bindings = KeyBindings() - - @bindings.add(" ") - @bindings.add("y") - @bindings.add("Y") - @bindings.add(Keys.ControlJ) - @bindings.add(Keys.ControlM) - @bindings.add(Keys.ControlI) # Tab. - def _yes(event: E) -> None: - event.app.exit(result=True) - - @bindings.add("n") - @bindings.add("N") - @bindings.add("q") - @bindings.add("Q") - @bindings.add(Keys.ControlC) - def _no(event: E) -> None: - event.app.exit(result=False) - - @bindings.add(Keys.Any) - def _ignore(event: E) -> None: - "Disable inserting of text." - - return PromptSession(message, key_bindings=bindings, erase_when_done=True) +""" +Key binding handlers for displaying completions. +""" +import asyncio +import math +from typing import TYPE_CHECKING, List + +from prompt_toolkit.application.run_in_terminal import in_terminal +from prompt_toolkit.completion import ( + CompleteEvent, + Completion, + get_common_complete_suffix, +) +from prompt_toolkit.formatted_text import StyleAndTextTuples +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.utils import get_cwidth + +if TYPE_CHECKING: + from prompt_toolkit.application import Application + from prompt_toolkit.shortcuts import PromptSession + +__all__ = [ + "generate_completions", + "display_completions_like_readline", +] + +E = KeyPressEvent + + +def generate_completions(event: E) -> None: + r""" + Tab-completion: where the first tab completes the common suffix and the + second tab lists all the completions. + """ + b = event.current_buffer + + # When already navigating through completions, select the next one. + if b.complete_state: + b.complete_next() + else: + b.start_completion(insert_common_part=True) + + +def display_completions_like_readline(event: E) -> None: + """ + Key binding handler for readline-style tab completion. + This is meant to be as similar as possible to the way how readline displays + completions. + + Generate the completions immediately (blocking) and display them above the + prompt in columns. + + Usage:: + + # Call this handler when 'Tab' has been pressed. + key_bindings.add(Keys.ControlI)(display_completions_like_readline) + """ + # Request completions. + b = event.current_buffer + if b.completer is None: + return + complete_event = CompleteEvent(completion_requested=True) + completions = list(b.completer.get_completions(b.document, complete_event)) + + # Calculate the common suffix. + common_suffix = get_common_complete_suffix(b.document, completions) + + # One completion: insert it. + if len(completions) == 1: + b.delete_before_cursor(-completions[0].start_position) + b.insert_text(completions[0].text) + # Multiple completions with common part. + elif common_suffix: + b.insert_text(common_suffix) + # Otherwise: display all completions. + elif completions: + _display_completions_like_readline(event.app, completions) + + +def _display_completions_like_readline( + app: "Application[object]", completions: List[Completion] +) -> "asyncio.Task[None]": + """ + Display the list of completions in columns above the prompt. + This will ask for a confirmation if there are too many completions to fit + on a single page and provide a paginator to walk through them. + """ + from prompt_toolkit.formatted_text import to_formatted_text + from prompt_toolkit.shortcuts.prompt import create_confirm_session + + # Get terminal dimensions. + term_size = app.output.get_size() + term_width = term_size.columns + term_height = term_size.rows + + # Calculate amount of required columns/rows for displaying the + # completions. (Keep in mind that completions are displayed + # alphabetically column-wise.) + max_compl_width = min( + term_width, max(get_cwidth(c.display_text) for c in completions) + 1 + ) + column_count = max(1, term_width // max_compl_width) + completions_per_page = column_count * (term_height - 1) + page_count = int(math.ceil(len(completions) / float(completions_per_page))) + # Note: math.ceil can return float on Python2. + + def display(page: int) -> None: + # Display completions. + page_completions = completions[ + page * completions_per_page : (page + 1) * completions_per_page + ] + + page_row_count = int(math.ceil(len(page_completions) / float(column_count))) + page_columns = [ + page_completions[i * page_row_count : (i + 1) * page_row_count] + for i in range(column_count) + ] + + result: StyleAndTextTuples = [] + + for r in range(page_row_count): + for c in range(column_count): + try: + completion = page_columns[c][r] + style = "class:readline-like-completions.completion " + ( + completion.style or "" + ) + + result.extend(to_formatted_text(completion.display, style=style)) + + # Add padding. + padding = max_compl_width - get_cwidth(completion.display_text) + result.append((completion.style, " " * padding)) + except IndexError: + pass + result.append(("", "\n")) + + app.print_text(to_formatted_text(result, "class:readline-like-completions")) + + # User interaction through an application generator function. + async def run_compl() -> None: + "Coroutine." + async with in_terminal(render_cli_done=True): + if len(completions) > completions_per_page: + # Ask confirmation if it doesn't fit on the screen. + confirm = await create_confirm_session( + "Display all {} possibilities?".format(len(completions)), + ).prompt_async() + + if confirm: + # Display pages. + for page in range(page_count): + display(page) + + if page != page_count - 1: + # Display --MORE-- and go to the next page. + show_more = await _create_more_session( + "--MORE--" + ).prompt_async() + + if not show_more: + return + else: + app.output.flush() + else: + # Display all completions. + display(0) + + return app.create_background_task(run_compl()) + + +def _create_more_session(message: str = "--MORE--") -> "PromptSession[bool]": + """ + Create a `PromptSession` object for displaying the "--MORE--". + """ + from prompt_toolkit.shortcuts import PromptSession + + bindings = KeyBindings() + + @bindings.add(" ") + @bindings.add("y") + @bindings.add("Y") + @bindings.add(Keys.ControlJ) + @bindings.add(Keys.ControlM) + @bindings.add(Keys.ControlI) # Tab. + def _yes(event: E) -> None: + event.app.exit(result=True) + + @bindings.add("n") + @bindings.add("N") + @bindings.add("q") + @bindings.add("Q") + @bindings.add(Keys.ControlC) + def _no(event: E) -> None: + event.app.exit(result=False) + + @bindings.add(Keys.Any) + def _ignore(event: E) -> None: + "Disable inserting of text." + + return PromptSession(message, key_bindings=bindings, erase_when_done=True) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/cpr.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/cpr.py index ff0f4aaaf1..07b0fa7527 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/cpr.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/cpr.py @@ -1,28 +1,28 @@ -from prompt_toolkit.key_binding.key_processor import KeyPressEvent -from prompt_toolkit.keys import Keys - -from ..key_bindings import KeyBindings - -__all__ = [ - "load_cpr_bindings", -] - -E = KeyPressEvent - - -def load_cpr_bindings() -> KeyBindings: - key_bindings = KeyBindings() - - @key_bindings.add(Keys.CPRResponse, save_before=lambda e: False) - def _(event: E) -> None: - """ - Handle incoming Cursor-Position-Request response. - """ - # The incoming data looks like u'\x1b[35;1R' - # Parse row/col information. - row, col = map(int, event.data[2:-1].split(";")) - - # Report absolute cursor position to the renderer. - event.app.renderer.report_absolute_cursor_row(row) - - return key_bindings +from prompt_toolkit.key_binding.key_processor import KeyPressEvent +from prompt_toolkit.keys import Keys + +from ..key_bindings import KeyBindings + +__all__ = [ + "load_cpr_bindings", +] + +E = KeyPressEvent + + +def load_cpr_bindings() -> KeyBindings: + key_bindings = KeyBindings() + + @key_bindings.add(Keys.CPRResponse, save_before=lambda e: False) + def _(event: E) -> None: + """ + Handle incoming Cursor-Position-Request response. + """ + # The incoming data looks like u'\x1b[35;1R' + # Parse row/col information. + row, col = map(int, event.data[2:-1].split(";")) + + # Report absolute cursor position to the renderer. + event.app.renderer.report_absolute_cursor_row(row) + + return key_bindings diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/emacs.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/emacs.py index 940144d8ff..a4a5e348f8 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/emacs.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/emacs.py @@ -1,557 +1,557 @@ -# pylint: disable=function-redefined -from typing import Dict, Union - -from prompt_toolkit.application.current import get_app -from prompt_toolkit.buffer import Buffer, indent, unindent -from prompt_toolkit.completion import CompleteEvent -from prompt_toolkit.filters import ( - Condition, - emacs_insert_mode, - emacs_mode, - has_arg, - has_selection, - in_paste_mode, - is_multiline, - is_read_only, - shift_selection_mode, - vi_search_direction_reversed, -) -from prompt_toolkit.key_binding.key_bindings import Binding -from prompt_toolkit.key_binding.key_processor import KeyPressEvent -from prompt_toolkit.keys import Keys -from prompt_toolkit.selection import SelectionType - -from ..key_bindings import ConditionalKeyBindings, KeyBindings, KeyBindingsBase -from .named_commands import get_by_name - -__all__ = [ - "load_emacs_bindings", - "load_emacs_search_bindings", - "load_emacs_shift_selection_bindings", -] - -E = KeyPressEvent - - -def load_emacs_bindings() -> KeyBindingsBase: - """ - Some e-macs extensions. - """ - # Overview of Readline emacs commands: - # http://www.catonmat.net/download/readline-emacs-editing-mode-cheat-sheet.pdf - key_bindings = KeyBindings() - handle = key_bindings.add - - insert_mode = emacs_insert_mode - - @handle("escape") - def _esc(event: E) -> None: - """ - By default, ignore escape key. - - (If we don't put this here, and Esc is followed by a key which sequence - is not handled, we'll insert an Escape character in the input stream. - Something we don't want and happens to easily in emacs mode. - Further, people can always use ControlQ to do a quoted insert.) - """ - pass - - handle("c-a")(get_by_name("beginning-of-line")) - handle("c-b")(get_by_name("backward-char")) - handle("c-delete", filter=insert_mode)(get_by_name("kill-word")) - handle("c-e")(get_by_name("end-of-line")) - handle("c-f")(get_by_name("forward-char")) - handle("c-left")(get_by_name("backward-word")) - handle("c-right")(get_by_name("forward-word")) - handle("c-x", "r", "y", filter=insert_mode)(get_by_name("yank")) - handle("c-y", filter=insert_mode)(get_by_name("yank")) - handle("escape", "b")(get_by_name("backward-word")) - handle("escape", "c", filter=insert_mode)(get_by_name("capitalize-word")) - handle("escape", "d", filter=insert_mode)(get_by_name("kill-word")) - handle("escape", "f")(get_by_name("forward-word")) - handle("escape", "l", filter=insert_mode)(get_by_name("downcase-word")) - handle("escape", "u", filter=insert_mode)(get_by_name("uppercase-word")) - handle("escape", "y", filter=insert_mode)(get_by_name("yank-pop")) - handle("escape", "backspace", filter=insert_mode)(get_by_name("backward-kill-word")) - handle("escape", "\\", filter=insert_mode)(get_by_name("delete-horizontal-space")) - - handle("c-home")(get_by_name("beginning-of-buffer")) - handle("c-end")(get_by_name("end-of-buffer")) - - handle("c-_", save_before=(lambda e: False), filter=insert_mode)( - get_by_name("undo") - ) - - handle("c-x", "c-u", save_before=(lambda e: False), filter=insert_mode)( - get_by_name("undo") - ) - - handle("escape", "<", filter=~has_selection)(get_by_name("beginning-of-history")) - handle("escape", ">", filter=~has_selection)(get_by_name("end-of-history")) - - handle("escape", ".", filter=insert_mode)(get_by_name("yank-last-arg")) - handle("escape", "_", filter=insert_mode)(get_by_name("yank-last-arg")) - handle("escape", "c-y", filter=insert_mode)(get_by_name("yank-nth-arg")) - handle("escape", "#", filter=insert_mode)(get_by_name("insert-comment")) - handle("c-o")(get_by_name("operate-and-get-next")) - - # ControlQ does a quoted insert. Not that for vt100 terminals, you have to - # disable flow control by running ``stty -ixon``, otherwise Ctrl-Q and - # Ctrl-S are captured by the terminal. - handle("c-q", filter=~has_selection)(get_by_name("quoted-insert")) - - handle("c-x", "(")(get_by_name("start-kbd-macro")) - handle("c-x", ")")(get_by_name("end-kbd-macro")) - handle("c-x", "e")(get_by_name("call-last-kbd-macro")) - - @handle("c-n") - def _next(event: E) -> None: - "Next line." - event.current_buffer.auto_down() - - @handle("c-p") - def _prev(event: E) -> None: - "Previous line." - event.current_buffer.auto_up(count=event.arg) - - def handle_digit(c: str) -> None: - """ - Handle input of arguments. - The first number needs to be preceded by escape. - """ - - @handle(c, filter=has_arg) - @handle("escape", c) - def _(event: E) -> None: - event.append_to_arg_count(c) - - for c in "0123456789": - handle_digit(c) - - @handle("escape", "-", filter=~has_arg) - def _meta_dash(event: E) -> None: - """""" - if event._arg is None: - event.append_to_arg_count("-") - - @handle("-", filter=Condition(lambda: get_app().key_processor.arg == "-")) - def _dash(event: E) -> None: - """ - When '-' is typed again, after exactly '-' has been given as an - argument, ignore this. - """ - event.app.key_processor.arg = "-" - - @Condition - def is_returnable() -> bool: - return get_app().current_buffer.is_returnable - - # Meta + Enter: always accept input. - handle("escape", "enter", filter=insert_mode & is_returnable)( - get_by_name("accept-line") - ) - - # Enter: accept input in single line mode. - handle("enter", filter=insert_mode & is_returnable & ~is_multiline)( - get_by_name("accept-line") - ) - - def character_search(buff: Buffer, char: str, count: int) -> None: - if count < 0: - match = buff.document.find_backwards( - char, in_current_line=True, count=-count - ) - else: - match = buff.document.find(char, in_current_line=True, count=count) - - if match is not None: - buff.cursor_position += match - - @handle("c-]", Keys.Any) - def _goto_char(event: E) -> None: - "When Ctl-] + a character is pressed. go to that character." - # Also named 'character-search' - character_search(event.current_buffer, event.data, event.arg) - - @handle("escape", "c-]", Keys.Any) - def _goto_char_backwards(event: E) -> None: - "Like Ctl-], but backwards." - # Also named 'character-search-backward' - character_search(event.current_buffer, event.data, -event.arg) - - @handle("escape", "a") - def _prev_sentence(event: E) -> None: - "Previous sentence." - # TODO: - - @handle("escape", "e") - def _end_of_sentence(event: E) -> None: - "Move to end of sentence." - # TODO: - - @handle("escape", "t", filter=insert_mode) - def _swap_characters(event: E) -> None: - """ - Swap the last two words before the cursor. - """ - # TODO - - @handle("escape", "*", filter=insert_mode) - def _insert_all_completions(event: E) -> None: - """ - `meta-*`: Insert all possible completions of the preceding text. - """ - buff = event.current_buffer - - # List all completions. - complete_event = CompleteEvent(text_inserted=False, completion_requested=True) - completions = list( - buff.completer.get_completions(buff.document, complete_event) - ) - - # Insert them. - text_to_insert = " ".join(c.text for c in completions) - buff.insert_text(text_to_insert) - - @handle("c-x", "c-x") - def _toggle_start_end(event: E) -> None: - """ - Move cursor back and forth between the start and end of the current - line. - """ - buffer = event.current_buffer - - if buffer.document.is_cursor_at_the_end_of_line: - buffer.cursor_position += buffer.document.get_start_of_line_position( - after_whitespace=False - ) - else: - buffer.cursor_position += buffer.document.get_end_of_line_position() - - @handle("c-@") # Control-space or Control-@ - def _start_selection(event: E) -> None: - """ - Start of the selection (if the current buffer is not empty). - """ - # Take the current cursor position as the start of this selection. - buff = event.current_buffer - if buff.text: - buff.start_selection(selection_type=SelectionType.CHARACTERS) - - @handle("c-g", filter=~has_selection) - def _cancel(event: E) -> None: - """ - Control + G: Cancel completion menu and validation state. - """ - event.current_buffer.complete_state = None - event.current_buffer.validation_error = None - - @handle("c-g", filter=has_selection) - def _cancel_selection(event: E) -> None: - """ - Cancel selection. - """ - event.current_buffer.exit_selection() - - @handle("c-w", filter=has_selection) - @handle("c-x", "r", "k", filter=has_selection) - def _cut(event: E) -> None: - """ - Cut selected text. - """ - data = event.current_buffer.cut_selection() - event.app.clipboard.set_data(data) - - @handle("escape", "w", filter=has_selection) - def _copy(event: E) -> None: - """ - Copy selected text. - """ - data = event.current_buffer.copy_selection() - event.app.clipboard.set_data(data) - - @handle("escape", "left") - def _start_of_word(event: E) -> None: - """ - Cursor to start of previous word. - """ - buffer = event.current_buffer - buffer.cursor_position += ( - buffer.document.find_previous_word_beginning(count=event.arg) or 0 - ) - - @handle("escape", "right") - def _start_next_word(event: E) -> None: - """ - Cursor to start of next word. - """ - buffer = event.current_buffer - buffer.cursor_position += ( - buffer.document.find_next_word_beginning(count=event.arg) - or buffer.document.get_end_of_document_position() - ) - - @handle("escape", "/", filter=insert_mode) - def _complete(event: E) -> None: - """ - M-/: Complete. - """ - b = event.current_buffer - if b.complete_state: - b.complete_next() - else: - b.start_completion(select_first=True) - - @handle("c-c", ">", filter=has_selection) - def _indent(event: E) -> None: - """ - Indent selected text. - """ - buffer = event.current_buffer - - buffer.cursor_position += buffer.document.get_start_of_line_position( - after_whitespace=True - ) - - from_, to = buffer.document.selection_range() - from_, _ = buffer.document.translate_index_to_position(from_) - to, _ = buffer.document.translate_index_to_position(to) - - indent(buffer, from_, to + 1, count=event.arg) - - @handle("c-c", "<", filter=has_selection) - def _unindent(event: E) -> None: - """ - Unindent selected text. - """ - buffer = event.current_buffer - - from_, to = buffer.document.selection_range() - from_, _ = buffer.document.translate_index_to_position(from_) - to, _ = buffer.document.translate_index_to_position(to) - - unindent(buffer, from_, to + 1, count=event.arg) - - return ConditionalKeyBindings(key_bindings, emacs_mode) - - -def load_emacs_search_bindings() -> KeyBindingsBase: - key_bindings = KeyBindings() - handle = key_bindings.add - from . import search - - # NOTE: We don't bind 'Escape' to 'abort_search'. The reason is that we - # want Alt+Enter to accept input directly in incremental search mode. - # Instead, we have double escape. - - handle("c-r")(search.start_reverse_incremental_search) - handle("c-s")(search.start_forward_incremental_search) - - handle("c-c")(search.abort_search) - handle("c-g")(search.abort_search) - handle("c-r")(search.reverse_incremental_search) - handle("c-s")(search.forward_incremental_search) - handle("up")(search.reverse_incremental_search) - handle("down")(search.forward_incremental_search) - handle("enter")(search.accept_search) - - # Handling of escape. - handle("escape", eager=True)(search.accept_search) - - # Like Readline, it's more natural to accept the search when escape has - # been pressed, however instead the following two bindings could be used - # instead. - # #handle('escape', 'escape', eager=True)(search.abort_search) - # #handle('escape', 'enter', eager=True)(search.accept_search_and_accept_input) - - # If Read-only: also include the following key bindings: - - # '/' and '?' key bindings for searching, just like Vi mode. - handle("?", filter=is_read_only & ~vi_search_direction_reversed)( - search.start_reverse_incremental_search - ) - handle("/", filter=is_read_only & ~vi_search_direction_reversed)( - search.start_forward_incremental_search - ) - handle("?", filter=is_read_only & vi_search_direction_reversed)( - search.start_forward_incremental_search - ) - handle("/", filter=is_read_only & vi_search_direction_reversed)( - search.start_reverse_incremental_search - ) - - @handle("n", filter=is_read_only) - def _jump_next(event: E) -> None: - "Jump to next match." - event.current_buffer.apply_search( - event.app.current_search_state, - include_current_position=False, - count=event.arg, - ) - - @handle("N", filter=is_read_only) - def _jump_prev(event: E) -> None: - "Jump to previous match." - event.current_buffer.apply_search( - ~event.app.current_search_state, - include_current_position=False, - count=event.arg, - ) - - return ConditionalKeyBindings(key_bindings, emacs_mode) - - -def load_emacs_shift_selection_bindings() -> KeyBindingsBase: - """ - Bindings to select text with shift + cursor movements - """ - - key_bindings = KeyBindings() - handle = key_bindings.add - - def unshift_move(event: E) -> None: - """ - Used for the shift selection mode. When called with - a shift + movement key press event, moves the cursor - as if shift is not pressed. - """ - key = event.key_sequence[0].key - - if key == Keys.ShiftUp: - event.current_buffer.auto_up(count=event.arg) - return - if key == Keys.ShiftDown: - event.current_buffer.auto_down(count=event.arg) - return - - # the other keys are handled through their readline command - key_to_command: Dict[Union[Keys, str], str] = { - Keys.ShiftLeft: "backward-char", - Keys.ShiftRight: "forward-char", - Keys.ShiftHome: "beginning-of-line", - Keys.ShiftEnd: "end-of-line", - Keys.ControlShiftLeft: "backward-word", - Keys.ControlShiftRight: "forward-word", - Keys.ControlShiftHome: "beginning-of-buffer", - Keys.ControlShiftEnd: "end-of-buffer", - } - - try: - # Both the dict lookup and `get_by_name` can raise KeyError. - binding = get_by_name(key_to_command[key]) - except KeyError: - pass - else: # (`else` is not really needed here.) - if isinstance(binding, Binding): - # (It should always be a binding here) - binding.call(event) - - @handle("s-left", filter=~has_selection) - @handle("s-right", filter=~has_selection) - @handle("s-up", filter=~has_selection) - @handle("s-down", filter=~has_selection) - @handle("s-home", filter=~has_selection) - @handle("s-end", filter=~has_selection) - @handle("c-s-left", filter=~has_selection) - @handle("c-s-right", filter=~has_selection) - @handle("c-s-home", filter=~has_selection) - @handle("c-s-end", filter=~has_selection) - def _start_selection(event: E) -> None: - """ - Start selection with shift + movement. - """ - # Take the current cursor position as the start of this selection. - buff = event.current_buffer - if buff.text: - buff.start_selection(selection_type=SelectionType.CHARACTERS) - - if buff.selection_state is not None: - # (`selection_state` should never be `None`, it is created by - # `start_selection`.) - buff.selection_state.enter_shift_mode() - - # Then move the cursor - original_position = buff.cursor_position - unshift_move(event) - if buff.cursor_position == original_position: - # Cursor didn't actually move - so cancel selection - # to avoid having an empty selection - buff.exit_selection() - - @handle("s-left", filter=shift_selection_mode) - @handle("s-right", filter=shift_selection_mode) - @handle("s-up", filter=shift_selection_mode) - @handle("s-down", filter=shift_selection_mode) - @handle("s-home", filter=shift_selection_mode) - @handle("s-end", filter=shift_selection_mode) - @handle("c-s-left", filter=shift_selection_mode) - @handle("c-s-right", filter=shift_selection_mode) - @handle("c-s-home", filter=shift_selection_mode) - @handle("c-s-end", filter=shift_selection_mode) - def _extend_selection(event: E) -> None: - """ - Extend the selection - """ - # Just move the cursor, like shift was not pressed - unshift_move(event) - buff = event.current_buffer - - if buff.selection_state is not None: - if buff.cursor_position == buff.selection_state.original_cursor_position: - # selection is now empty, so cancel selection - buff.exit_selection() - - @handle(Keys.Any, filter=shift_selection_mode) - def _replace_selection(event: E) -> None: - """ - Replace selection by what is typed - """ - event.current_buffer.cut_selection() - get_by_name("self-insert").call(event) - - @handle("enter", filter=shift_selection_mode & is_multiline) - def _newline(event: E) -> None: - """ - A newline replaces the selection - """ - event.current_buffer.cut_selection() - event.current_buffer.newline(copy_margin=not in_paste_mode()) - - @handle("backspace", filter=shift_selection_mode) - def _delete(event: E) -> None: - """ - Delete selection. - """ - event.current_buffer.cut_selection() - - @handle("c-y", filter=shift_selection_mode) - def _yank(event: E) -> None: - """ - In shift selection mode, yanking (pasting) replace the selection. - """ - buff = event.current_buffer - if buff.selection_state: - buff.cut_selection() - get_by_name("yank").call(event) - - # moving the cursor in shift selection mode cancels the selection - @handle("left", filter=shift_selection_mode) - @handle("right", filter=shift_selection_mode) - @handle("up", filter=shift_selection_mode) - @handle("down", filter=shift_selection_mode) - @handle("home", filter=shift_selection_mode) - @handle("end", filter=shift_selection_mode) - @handle("c-left", filter=shift_selection_mode) - @handle("c-right", filter=shift_selection_mode) - @handle("c-home", filter=shift_selection_mode) - @handle("c-end", filter=shift_selection_mode) - def _cancel(event: E) -> None: - """ - Cancel selection. - """ - event.current_buffer.exit_selection() - # we then process the cursor movement - key_press = event.key_sequence[0] - event.key_processor.feed(key_press, first=True) - - return ConditionalKeyBindings(key_bindings, emacs_mode) +# pylint: disable=function-redefined +from typing import Dict, Union + +from prompt_toolkit.application.current import get_app +from prompt_toolkit.buffer import Buffer, indent, unindent +from prompt_toolkit.completion import CompleteEvent +from prompt_toolkit.filters import ( + Condition, + emacs_insert_mode, + emacs_mode, + has_arg, + has_selection, + in_paste_mode, + is_multiline, + is_read_only, + shift_selection_mode, + vi_search_direction_reversed, +) +from prompt_toolkit.key_binding.key_bindings import Binding +from prompt_toolkit.key_binding.key_processor import KeyPressEvent +from prompt_toolkit.keys import Keys +from prompt_toolkit.selection import SelectionType + +from ..key_bindings import ConditionalKeyBindings, KeyBindings, KeyBindingsBase +from .named_commands import get_by_name + +__all__ = [ + "load_emacs_bindings", + "load_emacs_search_bindings", + "load_emacs_shift_selection_bindings", +] + +E = KeyPressEvent + + +def load_emacs_bindings() -> KeyBindingsBase: + """ + Some e-macs extensions. + """ + # Overview of Readline emacs commands: + # http://www.catonmat.net/download/readline-emacs-editing-mode-cheat-sheet.pdf + key_bindings = KeyBindings() + handle = key_bindings.add + + insert_mode = emacs_insert_mode + + @handle("escape") + def _esc(event: E) -> None: + """ + By default, ignore escape key. + + (If we don't put this here, and Esc is followed by a key which sequence + is not handled, we'll insert an Escape character in the input stream. + Something we don't want and happens to easily in emacs mode. + Further, people can always use ControlQ to do a quoted insert.) + """ + pass + + handle("c-a")(get_by_name("beginning-of-line")) + handle("c-b")(get_by_name("backward-char")) + handle("c-delete", filter=insert_mode)(get_by_name("kill-word")) + handle("c-e")(get_by_name("end-of-line")) + handle("c-f")(get_by_name("forward-char")) + handle("c-left")(get_by_name("backward-word")) + handle("c-right")(get_by_name("forward-word")) + handle("c-x", "r", "y", filter=insert_mode)(get_by_name("yank")) + handle("c-y", filter=insert_mode)(get_by_name("yank")) + handle("escape", "b")(get_by_name("backward-word")) + handle("escape", "c", filter=insert_mode)(get_by_name("capitalize-word")) + handle("escape", "d", filter=insert_mode)(get_by_name("kill-word")) + handle("escape", "f")(get_by_name("forward-word")) + handle("escape", "l", filter=insert_mode)(get_by_name("downcase-word")) + handle("escape", "u", filter=insert_mode)(get_by_name("uppercase-word")) + handle("escape", "y", filter=insert_mode)(get_by_name("yank-pop")) + handle("escape", "backspace", filter=insert_mode)(get_by_name("backward-kill-word")) + handle("escape", "\\", filter=insert_mode)(get_by_name("delete-horizontal-space")) + + handle("c-home")(get_by_name("beginning-of-buffer")) + handle("c-end")(get_by_name("end-of-buffer")) + + handle("c-_", save_before=(lambda e: False), filter=insert_mode)( + get_by_name("undo") + ) + + handle("c-x", "c-u", save_before=(lambda e: False), filter=insert_mode)( + get_by_name("undo") + ) + + handle("escape", "<", filter=~has_selection)(get_by_name("beginning-of-history")) + handle("escape", ">", filter=~has_selection)(get_by_name("end-of-history")) + + handle("escape", ".", filter=insert_mode)(get_by_name("yank-last-arg")) + handle("escape", "_", filter=insert_mode)(get_by_name("yank-last-arg")) + handle("escape", "c-y", filter=insert_mode)(get_by_name("yank-nth-arg")) + handle("escape", "#", filter=insert_mode)(get_by_name("insert-comment")) + handle("c-o")(get_by_name("operate-and-get-next")) + + # ControlQ does a quoted insert. Not that for vt100 terminals, you have to + # disable flow control by running ``stty -ixon``, otherwise Ctrl-Q and + # Ctrl-S are captured by the terminal. + handle("c-q", filter=~has_selection)(get_by_name("quoted-insert")) + + handle("c-x", "(")(get_by_name("start-kbd-macro")) + handle("c-x", ")")(get_by_name("end-kbd-macro")) + handle("c-x", "e")(get_by_name("call-last-kbd-macro")) + + @handle("c-n") + def _next(event: E) -> None: + "Next line." + event.current_buffer.auto_down() + + @handle("c-p") + def _prev(event: E) -> None: + "Previous line." + event.current_buffer.auto_up(count=event.arg) + + def handle_digit(c: str) -> None: + """ + Handle input of arguments. + The first number needs to be preceded by escape. + """ + + @handle(c, filter=has_arg) + @handle("escape", c) + def _(event: E) -> None: + event.append_to_arg_count(c) + + for c in "0123456789": + handle_digit(c) + + @handle("escape", "-", filter=~has_arg) + def _meta_dash(event: E) -> None: + """""" + if event._arg is None: + event.append_to_arg_count("-") + + @handle("-", filter=Condition(lambda: get_app().key_processor.arg == "-")) + def _dash(event: E) -> None: + """ + When '-' is typed again, after exactly '-' has been given as an + argument, ignore this. + """ + event.app.key_processor.arg = "-" + + @Condition + def is_returnable() -> bool: + return get_app().current_buffer.is_returnable + + # Meta + Enter: always accept input. + handle("escape", "enter", filter=insert_mode & is_returnable)( + get_by_name("accept-line") + ) + + # Enter: accept input in single line mode. + handle("enter", filter=insert_mode & is_returnable & ~is_multiline)( + get_by_name("accept-line") + ) + + def character_search(buff: Buffer, char: str, count: int) -> None: + if count < 0: + match = buff.document.find_backwards( + char, in_current_line=True, count=-count + ) + else: + match = buff.document.find(char, in_current_line=True, count=count) + + if match is not None: + buff.cursor_position += match + + @handle("c-]", Keys.Any) + def _goto_char(event: E) -> None: + "When Ctl-] + a character is pressed. go to that character." + # Also named 'character-search' + character_search(event.current_buffer, event.data, event.arg) + + @handle("escape", "c-]", Keys.Any) + def _goto_char_backwards(event: E) -> None: + "Like Ctl-], but backwards." + # Also named 'character-search-backward' + character_search(event.current_buffer, event.data, -event.arg) + + @handle("escape", "a") + def _prev_sentence(event: E) -> None: + "Previous sentence." + # TODO: + + @handle("escape", "e") + def _end_of_sentence(event: E) -> None: + "Move to end of sentence." + # TODO: + + @handle("escape", "t", filter=insert_mode) + def _swap_characters(event: E) -> None: + """ + Swap the last two words before the cursor. + """ + # TODO + + @handle("escape", "*", filter=insert_mode) + def _insert_all_completions(event: E) -> None: + """ + `meta-*`: Insert all possible completions of the preceding text. + """ + buff = event.current_buffer + + # List all completions. + complete_event = CompleteEvent(text_inserted=False, completion_requested=True) + completions = list( + buff.completer.get_completions(buff.document, complete_event) + ) + + # Insert them. + text_to_insert = " ".join(c.text for c in completions) + buff.insert_text(text_to_insert) + + @handle("c-x", "c-x") + def _toggle_start_end(event: E) -> None: + """ + Move cursor back and forth between the start and end of the current + line. + """ + buffer = event.current_buffer + + if buffer.document.is_cursor_at_the_end_of_line: + buffer.cursor_position += buffer.document.get_start_of_line_position( + after_whitespace=False + ) + else: + buffer.cursor_position += buffer.document.get_end_of_line_position() + + @handle("c-@") # Control-space or Control-@ + def _start_selection(event: E) -> None: + """ + Start of the selection (if the current buffer is not empty). + """ + # Take the current cursor position as the start of this selection. + buff = event.current_buffer + if buff.text: + buff.start_selection(selection_type=SelectionType.CHARACTERS) + + @handle("c-g", filter=~has_selection) + def _cancel(event: E) -> None: + """ + Control + G: Cancel completion menu and validation state. + """ + event.current_buffer.complete_state = None + event.current_buffer.validation_error = None + + @handle("c-g", filter=has_selection) + def _cancel_selection(event: E) -> None: + """ + Cancel selection. + """ + event.current_buffer.exit_selection() + + @handle("c-w", filter=has_selection) + @handle("c-x", "r", "k", filter=has_selection) + def _cut(event: E) -> None: + """ + Cut selected text. + """ + data = event.current_buffer.cut_selection() + event.app.clipboard.set_data(data) + + @handle("escape", "w", filter=has_selection) + def _copy(event: E) -> None: + """ + Copy selected text. + """ + data = event.current_buffer.copy_selection() + event.app.clipboard.set_data(data) + + @handle("escape", "left") + def _start_of_word(event: E) -> None: + """ + Cursor to start of previous word. + """ + buffer = event.current_buffer + buffer.cursor_position += ( + buffer.document.find_previous_word_beginning(count=event.arg) or 0 + ) + + @handle("escape", "right") + def _start_next_word(event: E) -> None: + """ + Cursor to start of next word. + """ + buffer = event.current_buffer + buffer.cursor_position += ( + buffer.document.find_next_word_beginning(count=event.arg) + or buffer.document.get_end_of_document_position() + ) + + @handle("escape", "/", filter=insert_mode) + def _complete(event: E) -> None: + """ + M-/: Complete. + """ + b = event.current_buffer + if b.complete_state: + b.complete_next() + else: + b.start_completion(select_first=True) + + @handle("c-c", ">", filter=has_selection) + def _indent(event: E) -> None: + """ + Indent selected text. + """ + buffer = event.current_buffer + + buffer.cursor_position += buffer.document.get_start_of_line_position( + after_whitespace=True + ) + + from_, to = buffer.document.selection_range() + from_, _ = buffer.document.translate_index_to_position(from_) + to, _ = buffer.document.translate_index_to_position(to) + + indent(buffer, from_, to + 1, count=event.arg) + + @handle("c-c", "<", filter=has_selection) + def _unindent(event: E) -> None: + """ + Unindent selected text. + """ + buffer = event.current_buffer + + from_, to = buffer.document.selection_range() + from_, _ = buffer.document.translate_index_to_position(from_) + to, _ = buffer.document.translate_index_to_position(to) + + unindent(buffer, from_, to + 1, count=event.arg) + + return ConditionalKeyBindings(key_bindings, emacs_mode) + + +def load_emacs_search_bindings() -> KeyBindingsBase: + key_bindings = KeyBindings() + handle = key_bindings.add + from . import search + + # NOTE: We don't bind 'Escape' to 'abort_search'. The reason is that we + # want Alt+Enter to accept input directly in incremental search mode. + # Instead, we have double escape. + + handle("c-r")(search.start_reverse_incremental_search) + handle("c-s")(search.start_forward_incremental_search) + + handle("c-c")(search.abort_search) + handle("c-g")(search.abort_search) + handle("c-r")(search.reverse_incremental_search) + handle("c-s")(search.forward_incremental_search) + handle("up")(search.reverse_incremental_search) + handle("down")(search.forward_incremental_search) + handle("enter")(search.accept_search) + + # Handling of escape. + handle("escape", eager=True)(search.accept_search) + + # Like Readline, it's more natural to accept the search when escape has + # been pressed, however instead the following two bindings could be used + # instead. + # #handle('escape', 'escape', eager=True)(search.abort_search) + # #handle('escape', 'enter', eager=True)(search.accept_search_and_accept_input) + + # If Read-only: also include the following key bindings: + + # '/' and '?' key bindings for searching, just like Vi mode. + handle("?", filter=is_read_only & ~vi_search_direction_reversed)( + search.start_reverse_incremental_search + ) + handle("/", filter=is_read_only & ~vi_search_direction_reversed)( + search.start_forward_incremental_search + ) + handle("?", filter=is_read_only & vi_search_direction_reversed)( + search.start_forward_incremental_search + ) + handle("/", filter=is_read_only & vi_search_direction_reversed)( + search.start_reverse_incremental_search + ) + + @handle("n", filter=is_read_only) + def _jump_next(event: E) -> None: + "Jump to next match." + event.current_buffer.apply_search( + event.app.current_search_state, + include_current_position=False, + count=event.arg, + ) + + @handle("N", filter=is_read_only) + def _jump_prev(event: E) -> None: + "Jump to previous match." + event.current_buffer.apply_search( + ~event.app.current_search_state, + include_current_position=False, + count=event.arg, + ) + + return ConditionalKeyBindings(key_bindings, emacs_mode) + + +def load_emacs_shift_selection_bindings() -> KeyBindingsBase: + """ + Bindings to select text with shift + cursor movements + """ + + key_bindings = KeyBindings() + handle = key_bindings.add + + def unshift_move(event: E) -> None: + """ + Used for the shift selection mode. When called with + a shift + movement key press event, moves the cursor + as if shift is not pressed. + """ + key = event.key_sequence[0].key + + if key == Keys.ShiftUp: + event.current_buffer.auto_up(count=event.arg) + return + if key == Keys.ShiftDown: + event.current_buffer.auto_down(count=event.arg) + return + + # the other keys are handled through their readline command + key_to_command: Dict[Union[Keys, str], str] = { + Keys.ShiftLeft: "backward-char", + Keys.ShiftRight: "forward-char", + Keys.ShiftHome: "beginning-of-line", + Keys.ShiftEnd: "end-of-line", + Keys.ControlShiftLeft: "backward-word", + Keys.ControlShiftRight: "forward-word", + Keys.ControlShiftHome: "beginning-of-buffer", + Keys.ControlShiftEnd: "end-of-buffer", + } + + try: + # Both the dict lookup and `get_by_name` can raise KeyError. + binding = get_by_name(key_to_command[key]) + except KeyError: + pass + else: # (`else` is not really needed here.) + if isinstance(binding, Binding): + # (It should always be a binding here) + binding.call(event) + + @handle("s-left", filter=~has_selection) + @handle("s-right", filter=~has_selection) + @handle("s-up", filter=~has_selection) + @handle("s-down", filter=~has_selection) + @handle("s-home", filter=~has_selection) + @handle("s-end", filter=~has_selection) + @handle("c-s-left", filter=~has_selection) + @handle("c-s-right", filter=~has_selection) + @handle("c-s-home", filter=~has_selection) + @handle("c-s-end", filter=~has_selection) + def _start_selection(event: E) -> None: + """ + Start selection with shift + movement. + """ + # Take the current cursor position as the start of this selection. + buff = event.current_buffer + if buff.text: + buff.start_selection(selection_type=SelectionType.CHARACTERS) + + if buff.selection_state is not None: + # (`selection_state` should never be `None`, it is created by + # `start_selection`.) + buff.selection_state.enter_shift_mode() + + # Then move the cursor + original_position = buff.cursor_position + unshift_move(event) + if buff.cursor_position == original_position: + # Cursor didn't actually move - so cancel selection + # to avoid having an empty selection + buff.exit_selection() + + @handle("s-left", filter=shift_selection_mode) + @handle("s-right", filter=shift_selection_mode) + @handle("s-up", filter=shift_selection_mode) + @handle("s-down", filter=shift_selection_mode) + @handle("s-home", filter=shift_selection_mode) + @handle("s-end", filter=shift_selection_mode) + @handle("c-s-left", filter=shift_selection_mode) + @handle("c-s-right", filter=shift_selection_mode) + @handle("c-s-home", filter=shift_selection_mode) + @handle("c-s-end", filter=shift_selection_mode) + def _extend_selection(event: E) -> None: + """ + Extend the selection + """ + # Just move the cursor, like shift was not pressed + unshift_move(event) + buff = event.current_buffer + + if buff.selection_state is not None: + if buff.cursor_position == buff.selection_state.original_cursor_position: + # selection is now empty, so cancel selection + buff.exit_selection() + + @handle(Keys.Any, filter=shift_selection_mode) + def _replace_selection(event: E) -> None: + """ + Replace selection by what is typed + """ + event.current_buffer.cut_selection() + get_by_name("self-insert").call(event) + + @handle("enter", filter=shift_selection_mode & is_multiline) + def _newline(event: E) -> None: + """ + A newline replaces the selection + """ + event.current_buffer.cut_selection() + event.current_buffer.newline(copy_margin=not in_paste_mode()) + + @handle("backspace", filter=shift_selection_mode) + def _delete(event: E) -> None: + """ + Delete selection. + """ + event.current_buffer.cut_selection() + + @handle("c-y", filter=shift_selection_mode) + def _yank(event: E) -> None: + """ + In shift selection mode, yanking (pasting) replace the selection. + """ + buff = event.current_buffer + if buff.selection_state: + buff.cut_selection() + get_by_name("yank").call(event) + + # moving the cursor in shift selection mode cancels the selection + @handle("left", filter=shift_selection_mode) + @handle("right", filter=shift_selection_mode) + @handle("up", filter=shift_selection_mode) + @handle("down", filter=shift_selection_mode) + @handle("home", filter=shift_selection_mode) + @handle("end", filter=shift_selection_mode) + @handle("c-left", filter=shift_selection_mode) + @handle("c-right", filter=shift_selection_mode) + @handle("c-home", filter=shift_selection_mode) + @handle("c-end", filter=shift_selection_mode) + def _cancel(event: E) -> None: + """ + Cancel selection. + """ + event.current_buffer.exit_selection() + # we then process the cursor movement + key_press = event.key_sequence[0] + event.key_processor.feed(key_press, first=True) + + return ConditionalKeyBindings(key_bindings, emacs_mode) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/focus.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/focus.py index 30725a310f..40844db641 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/focus.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/focus.py @@ -1,24 +1,24 @@ -from prompt_toolkit.key_binding.key_processor import KeyPressEvent - -__all__ = [ - "focus_next", - "focus_previous", -] - -E = KeyPressEvent - - -def focus_next(event: E) -> None: - """ - Focus the next visible Window. - (Often bound to the `Tab` key.) - """ - event.app.layout.focus_next() - - -def focus_previous(event: E) -> None: - """ - Focus the previous visible Window. - (Often bound to the `BackTab` key.) - """ - event.app.layout.focus_previous() +from prompt_toolkit.key_binding.key_processor import KeyPressEvent + +__all__ = [ + "focus_next", + "focus_previous", +] + +E = KeyPressEvent + + +def focus_next(event: E) -> None: + """ + Focus the next visible Window. + (Often bound to the `Tab` key.) + """ + event.app.layout.focus_next() + + +def focus_previous(event: E) -> None: + """ + Focus the previous visible Window. + (Often bound to the `BackTab` key.) + """ + event.app.layout.focus_previous() diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/mouse.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/mouse.py index bf9f644beb..949c33f72c 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/mouse.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/mouse.py @@ -1,346 +1,346 @@ -from typing import TYPE_CHECKING, FrozenSet - -from prompt_toolkit.data_structures import Point -from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent -from prompt_toolkit.keys import Keys -from prompt_toolkit.mouse_events import ( - MouseButton, - MouseEvent, - MouseEventType, - MouseModifier, -) -from prompt_toolkit.utils import is_windows - -from ..key_bindings import KeyBindings - -if TYPE_CHECKING: - from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone - -__all__ = [ - "load_mouse_bindings", -] - -E = KeyPressEvent - -# fmt: off -# flake8: noqa E201 -SCROLL_UP = MouseEventType.SCROLL_UP -SCROLL_DOWN = MouseEventType.SCROLL_DOWN -MOUSE_DOWN = MouseEventType.MOUSE_DOWN -MOUSE_MOVE = MouseEventType.MOUSE_MOVE -MOUSE_UP = MouseEventType.MOUSE_UP - -NO_MODIFIER : FrozenSet[MouseModifier] = frozenset() -SHIFT : FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT}) -ALT : FrozenSet[MouseModifier] = frozenset({MouseModifier.ALT}) -SHIFT_ALT : FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT, MouseModifier.ALT}) -CONTROL : FrozenSet[MouseModifier] = frozenset({MouseModifier.CONTROL}) -SHIFT_CONTROL : FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT, MouseModifier.CONTROL}) -ALT_CONTROL : FrozenSet[MouseModifier] = frozenset({MouseModifier.ALT, MouseModifier.CONTROL}) -SHIFT_ALT_CONTROL: FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT, MouseModifier.ALT, MouseModifier.CONTROL}) -UNKNOWN_MODIFIER : FrozenSet[MouseModifier] = frozenset() - -LEFT = MouseButton.LEFT -MIDDLE = MouseButton.MIDDLE -RIGHT = MouseButton.RIGHT -NO_BUTTON = MouseButton.NONE -UNKNOWN_BUTTON = MouseButton.UNKNOWN - -xterm_sgr_mouse_events = { - ( 0, 'm') : (LEFT, MOUSE_UP, NO_MODIFIER), # left_up 0+ + + =0 - ( 4, 'm') : (LEFT, MOUSE_UP, SHIFT), # left_up Shift 0+4+ + =4 - ( 8, 'm') : (LEFT, MOUSE_UP, ALT), # left_up Alt 0+ +8+ =8 - (12, 'm') : (LEFT, MOUSE_UP, SHIFT_ALT), # left_up Shift Alt 0+4+8+ =12 - (16, 'm') : (LEFT, MOUSE_UP, CONTROL), # left_up Control 0+ + +16=16 - (20, 'm') : (LEFT, MOUSE_UP, SHIFT_CONTROL), # left_up Shift Control 0+4+ +16=20 - (24, 'm') : (LEFT, MOUSE_UP, ALT_CONTROL), # left_up Alt Control 0+ +8+16=24 - (28, 'm') : (LEFT, MOUSE_UP, SHIFT_ALT_CONTROL), # left_up Shift Alt Control 0+4+8+16=28 - - ( 1, 'm') : (MIDDLE, MOUSE_UP, NO_MODIFIER), # middle_up 1+ + + =1 - ( 5, 'm') : (MIDDLE, MOUSE_UP, SHIFT), # middle_up Shift 1+4+ + =5 - ( 9, 'm') : (MIDDLE, MOUSE_UP, ALT), # middle_up Alt 1+ +8+ =9 - (13, 'm') : (MIDDLE, MOUSE_UP, SHIFT_ALT), # middle_up Shift Alt 1+4+8+ =13 - (17, 'm') : (MIDDLE, MOUSE_UP, CONTROL), # middle_up Control 1+ + +16=17 - (21, 'm') : (MIDDLE, MOUSE_UP, SHIFT_CONTROL), # middle_up Shift Control 1+4+ +16=21 - (25, 'm') : (MIDDLE, MOUSE_UP, ALT_CONTROL), # middle_up Alt Control 1+ +8+16=25 - (29, 'm') : (MIDDLE, MOUSE_UP, SHIFT_ALT_CONTROL), # middle_up Shift Alt Control 1+4+8+16=29 - - ( 2, 'm') : (RIGHT, MOUSE_UP, NO_MODIFIER), # right_up 2+ + + =2 - ( 6, 'm') : (RIGHT, MOUSE_UP, SHIFT), # right_up Shift 2+4+ + =6 - (10, 'm') : (RIGHT, MOUSE_UP, ALT), # right_up Alt 2+ +8+ =10 - (14, 'm') : (RIGHT, MOUSE_UP, SHIFT_ALT), # right_up Shift Alt 2+4+8+ =14 - (18, 'm') : (RIGHT, MOUSE_UP, CONTROL), # right_up Control 2+ + +16=18 - (22, 'm') : (RIGHT, MOUSE_UP, SHIFT_CONTROL), # right_up Shift Control 2+4+ +16=22 - (26, 'm') : (RIGHT, MOUSE_UP, ALT_CONTROL), # right_up Alt Control 2+ +8+16=26 - (30, 'm') : (RIGHT, MOUSE_UP, SHIFT_ALT_CONTROL), # right_up Shift Alt Control 2+4+8+16=30 - - ( 0, 'M') : (LEFT, MOUSE_DOWN, NO_MODIFIER), # left_down 0+ + + =0 - ( 4, 'M') : (LEFT, MOUSE_DOWN, SHIFT), # left_down Shift 0+4+ + =4 - ( 8, 'M') : (LEFT, MOUSE_DOWN, ALT), # left_down Alt 0+ +8+ =8 - (12, 'M') : (LEFT, MOUSE_DOWN, SHIFT_ALT), # left_down Shift Alt 0+4+8+ =12 - (16, 'M') : (LEFT, MOUSE_DOWN, CONTROL), # left_down Control 0+ + +16=16 - (20, 'M') : (LEFT, MOUSE_DOWN, SHIFT_CONTROL), # left_down Shift Control 0+4+ +16=20 - (24, 'M') : (LEFT, MOUSE_DOWN, ALT_CONTROL), # left_down Alt Control 0+ +8+16=24 - (28, 'M') : (LEFT, MOUSE_DOWN, SHIFT_ALT_CONTROL), # left_down Shift Alt Control 0+4+8+16=28 - - ( 1, 'M') : (MIDDLE, MOUSE_DOWN, NO_MODIFIER), # middle_down 1+ + + =1 - ( 5, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT), # middle_down Shift 1+4+ + =5 - ( 9, 'M') : (MIDDLE, MOUSE_DOWN, ALT), # middle_down Alt 1+ +8+ =9 - (13, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT_ALT), # middle_down Shift Alt 1+4+8+ =13 - (17, 'M') : (MIDDLE, MOUSE_DOWN, CONTROL), # middle_down Control 1+ + +16=17 - (21, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT_CONTROL), # middle_down Shift Control 1+4+ +16=21 - (25, 'M') : (MIDDLE, MOUSE_DOWN, ALT_CONTROL), # middle_down Alt Control 1+ +8+16=25 - (29, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT_ALT_CONTROL), # middle_down Shift Alt Control 1+4+8+16=29 - - ( 2, 'M') : (RIGHT, MOUSE_DOWN, NO_MODIFIER), # right_down 2+ + + =2 - ( 6, 'M') : (RIGHT, MOUSE_DOWN, SHIFT), # right_down Shift 2+4+ + =6 - (10, 'M') : (RIGHT, MOUSE_DOWN, ALT), # right_down Alt 2+ +8+ =10 - (14, 'M') : (RIGHT, MOUSE_DOWN, SHIFT_ALT), # right_down Shift Alt 2+4+8+ =14 - (18, 'M') : (RIGHT, MOUSE_DOWN, CONTROL), # right_down Control 2+ + +16=18 - (22, 'M') : (RIGHT, MOUSE_DOWN, SHIFT_CONTROL), # right_down Shift Control 2+4+ +16=22 - (26, 'M') : (RIGHT, MOUSE_DOWN, ALT_CONTROL), # right_down Alt Control 2+ +8+16=26 - (30, 'M') : (RIGHT, MOUSE_DOWN, SHIFT_ALT_CONTROL), # right_down Shift Alt Control 2+4+8+16=30 - - (32, 'M') : (LEFT, MOUSE_MOVE, NO_MODIFIER), # left_drag 32+ + + =32 - (36, 'M') : (LEFT, MOUSE_MOVE, SHIFT), # left_drag Shift 32+4+ + =36 - (40, 'M') : (LEFT, MOUSE_MOVE, ALT), # left_drag Alt 32+ +8+ =40 - (44, 'M') : (LEFT, MOUSE_MOVE, SHIFT_ALT), # left_drag Shift Alt 32+4+8+ =44 - (48, 'M') : (LEFT, MOUSE_MOVE, CONTROL), # left_drag Control 32+ + +16=48 - (52, 'M') : (LEFT, MOUSE_MOVE, SHIFT_CONTROL), # left_drag Shift Control 32+4+ +16=52 - (56, 'M') : (LEFT, MOUSE_MOVE, ALT_CONTROL), # left_drag Alt Control 32+ +8+16=56 - (60, 'M') : (LEFT, MOUSE_MOVE, SHIFT_ALT_CONTROL), # left_drag Shift Alt Control 32+4+8+16=60 - - (33, 'M') : (MIDDLE, MOUSE_MOVE, NO_MODIFIER), # middle_drag 33+ + + =33 - (37, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT), # middle_drag Shift 33+4+ + =37 - (41, 'M') : (MIDDLE, MOUSE_MOVE, ALT), # middle_drag Alt 33+ +8+ =41 - (45, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT_ALT), # middle_drag Shift Alt 33+4+8+ =45 - (49, 'M') : (MIDDLE, MOUSE_MOVE, CONTROL), # middle_drag Control 33+ + +16=49 - (53, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT_CONTROL), # middle_drag Shift Control 33+4+ +16=53 - (57, 'M') : (MIDDLE, MOUSE_MOVE, ALT_CONTROL), # middle_drag Alt Control 33+ +8+16=57 - (61, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT_ALT_CONTROL), # middle_drag Shift Alt Control 33+4+8+16=61 - - (34, 'M') : (RIGHT, MOUSE_MOVE, NO_MODIFIER), # right_drag 34+ + + =34 - (38, 'M') : (RIGHT, MOUSE_MOVE, SHIFT), # right_drag Shift 34+4+ + =38 - (42, 'M') : (RIGHT, MOUSE_MOVE, ALT), # right_drag Alt 34+ +8+ =42 - (46, 'M') : (RIGHT, MOUSE_MOVE, SHIFT_ALT), # right_drag Shift Alt 34+4+8+ =46 - (50, 'M') : (RIGHT, MOUSE_MOVE, CONTROL), # right_drag Control 34+ + +16=50 - (54, 'M') : (RIGHT, MOUSE_MOVE, SHIFT_CONTROL), # right_drag Shift Control 34+4+ +16=54 - (58, 'M') : (RIGHT, MOUSE_MOVE, ALT_CONTROL), # right_drag Alt Control 34+ +8+16=58 - (62, 'M') : (RIGHT, MOUSE_MOVE, SHIFT_ALT_CONTROL), # right_drag Shift Alt Control 34+4+8+16=62 - - (35, 'M') : (NO_BUTTON, MOUSE_MOVE, NO_MODIFIER), # none_drag 35+ + + =35 - (39, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT), # none_drag Shift 35+4+ + =39 - (43, 'M') : (NO_BUTTON, MOUSE_MOVE, ALT), # none_drag Alt 35+ +8+ =43 - (47, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT_ALT), # none_drag Shift Alt 35+4+8+ =47 - (51, 'M') : (NO_BUTTON, MOUSE_MOVE, CONTROL), # none_drag Control 35+ + +16=51 - (55, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT_CONTROL), # none_drag Shift Control 35+4+ +16=55 - (59, 'M') : (NO_BUTTON, MOUSE_MOVE, ALT_CONTROL), # none_drag Alt Control 35+ +8+16=59 - (63, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT_ALT_CONTROL), # none_drag Shift Alt Control 35+4+8+16=63 - - (64, 'M') : (NO_BUTTON, SCROLL_UP, NO_MODIFIER), # scroll_up 64+ + + =64 - (68, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT), # scroll_up Shift 64+4+ + =68 - (72, 'M') : (NO_BUTTON, SCROLL_UP, ALT), # scroll_up Alt 64+ +8+ =72 - (76, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT_ALT), # scroll_up Shift Alt 64+4+8+ =76 - (80, 'M') : (NO_BUTTON, SCROLL_UP, CONTROL), # scroll_up Control 64+ + +16=80 - (84, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT_CONTROL), # scroll_up Shift Control 64+4+ +16=84 - (88, 'M') : (NO_BUTTON, SCROLL_UP, ALT_CONTROL), # scroll_up Alt Control 64+ +8+16=88 - (92, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT_ALT_CONTROL), # scroll_up Shift Alt Control 64+4+8+16=92 - - (65, 'M') : (NO_BUTTON, SCROLL_DOWN, NO_MODIFIER), # scroll_down 64+ + + =65 - (69, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT), # scroll_down Shift 64+4+ + =69 - (73, 'M') : (NO_BUTTON, SCROLL_DOWN, ALT), # scroll_down Alt 64+ +8+ =73 - (77, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT_ALT), # scroll_down Shift Alt 64+4+8+ =77 - (81, 'M') : (NO_BUTTON, SCROLL_DOWN, CONTROL), # scroll_down Control 64+ + +16=81 - (85, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT_CONTROL), # scroll_down Shift Control 64+4+ +16=85 - (89, 'M') : (NO_BUTTON, SCROLL_DOWN, ALT_CONTROL), # scroll_down Alt Control 64+ +8+16=89 - (93, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT_ALT_CONTROL), # scroll_down Shift Alt Control 64+4+8+16=93 -} - -typical_mouse_events = { - 32: (LEFT , MOUSE_DOWN , UNKNOWN_MODIFIER), - 33: (MIDDLE , MOUSE_DOWN , UNKNOWN_MODIFIER), - 34: (RIGHT , MOUSE_DOWN , UNKNOWN_MODIFIER), - 35: (UNKNOWN_BUTTON , MOUSE_UP , UNKNOWN_MODIFIER), - - 64: (LEFT , MOUSE_MOVE , UNKNOWN_MODIFIER), - 65: (MIDDLE , MOUSE_MOVE , UNKNOWN_MODIFIER), - 66: (RIGHT , MOUSE_MOVE , UNKNOWN_MODIFIER), - 67: (NO_BUTTON , MOUSE_MOVE , UNKNOWN_MODIFIER), - - 96: (NO_BUTTON , SCROLL_UP , UNKNOWN_MODIFIER), - 97: (NO_BUTTON , SCROLL_DOWN, UNKNOWN_MODIFIER), -} - -urxvt_mouse_events={ - 32: (UNKNOWN_BUTTON, MOUSE_DOWN , UNKNOWN_MODIFIER), - 35: (UNKNOWN_BUTTON, MOUSE_UP , UNKNOWN_MODIFIER), - 96: (NO_BUTTON , SCROLL_UP , UNKNOWN_MODIFIER), - 97: (NO_BUTTON , SCROLL_DOWN, UNKNOWN_MODIFIER), -} -# fmt:on - - -def load_mouse_bindings() -> KeyBindings: - """ - Key bindings, required for mouse support. - (Mouse events enter through the key binding system.) - """ - key_bindings = KeyBindings() - - @key_bindings.add(Keys.Vt100MouseEvent) - def _(event: E) -> "NotImplementedOrNone": - """ - Handling of incoming mouse event. - """ - # TypicaL: "eSC[MaB*" - # Urxvt: "Esc[96;14;13M" - # Xterm SGR: "Esc[<64;85;12M" - - # Parse incoming packet. - if event.data[2] == "M": - # Typical. - mouse_event, x, y = map(ord, event.data[3:]) - - # TODO: Is it possible to add modifiers here? - mouse_button, mouse_event_type, mouse_modifier = typical_mouse_events[ - mouse_event - ] - - # Handle situations where `PosixStdinReader` used surrogateescapes. - if x >= 0xDC00: - x -= 0xDC00 - if y >= 0xDC00: - y -= 0xDC00 - - x -= 32 - y -= 32 - else: - # Urxvt and Xterm SGR. - # When the '<' is not present, we are not using the Xterm SGR mode, - # but Urxvt instead. - data = event.data[2:] - if data[:1] == "<": - sgr = True - data = data[1:] - else: - sgr = False - - # Extract coordinates. - mouse_event, x, y = map(int, data[:-1].split(";")) - m = data[-1] - - # Parse event type. - if sgr: - try: - ( - mouse_button, - mouse_event_type, - mouse_modifiers, - ) = xterm_sgr_mouse_events[mouse_event, m] - except KeyError: - return NotImplemented - - else: - # Some other terminals, like urxvt, Hyper terminal, ... - ( - mouse_button, - mouse_event_type, - mouse_modifiers, - ) = urxvt_mouse_events.get( - mouse_event, (UNKNOWN_BUTTON, MOUSE_MOVE, UNKNOWN_MODIFIER) - ) - - x -= 1 - y -= 1 - - # Only handle mouse events when we know the window height. - if event.app.renderer.height_is_known and mouse_event_type is not None: - # Take region above the layout into account. The reported - # coordinates are absolute to the visible part of the terminal. - from prompt_toolkit.renderer import HeightIsUnknownError - - try: - y -= event.app.renderer.rows_above_layout - except HeightIsUnknownError: - return NotImplemented - - # Call the mouse handler from the renderer. - - # Note: This can return `NotImplemented` if no mouse handler was - # found for this position, or if no repainting needs to - # happen. this way, we avoid excessive repaints during mouse - # movements. - handler = event.app.renderer.mouse_handlers.mouse_handlers[y][x] - return handler( - MouseEvent( - position=Point(x=x, y=y), - event_type=mouse_event_type, - button=mouse_button, - modifiers=mouse_modifiers, - ) - ) - - return NotImplemented - - @key_bindings.add(Keys.ScrollUp) - def _scroll_up(event: E) -> None: - """ - Scroll up event without cursor position. - """ - # We don't receive a cursor position, so we don't know which window to - # scroll. Just send an 'up' key press instead. - event.key_processor.feed(KeyPress(Keys.Up), first=True) - - @key_bindings.add(Keys.ScrollDown) - def _scroll_down(event: E) -> None: - """ - Scroll down event without cursor position. - """ - event.key_processor.feed(KeyPress(Keys.Down), first=True) - - @key_bindings.add(Keys.WindowsMouseEvent) - def _mouse(event: E) -> "NotImplementedOrNone": - """ - Handling of mouse events for Windows. - """ - assert is_windows() # This key binding should only exist for Windows. - - # Parse data. - pieces = event.data.split(";") - - button = MouseButton(pieces[0]) - event_type = MouseEventType(pieces[1]) - x = int(pieces[2]) - y = int(pieces[3]) - - # Make coordinates absolute to the visible part of the terminal. - output = event.app.renderer.output - - from prompt_toolkit.output.win32 import Win32Output - from prompt_toolkit.output.windows10 import Windows10_Output - - if isinstance(output, (Win32Output, Windows10_Output)): - screen_buffer_info = output.get_win32_screen_buffer_info() - rows_above_cursor = ( - screen_buffer_info.dwCursorPosition.Y - event.app.renderer._cursor_pos.y - ) - y -= rows_above_cursor - - # Call the mouse event handler. - # (Can return `NotImplemented`.) - handler = event.app.renderer.mouse_handlers.mouse_handlers[y][x] - - return handler( - MouseEvent( - position=Point(x=x, y=y), - event_type=event_type, - button=button, - modifiers=UNKNOWN_MODIFIER, - ) - ) - - # No mouse handler found. Return `NotImplemented` so that we don't - # invalidate the UI. - return NotImplemented - - return key_bindings +from typing import TYPE_CHECKING, FrozenSet + +from prompt_toolkit.data_structures import Point +from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent +from prompt_toolkit.keys import Keys +from prompt_toolkit.mouse_events import ( + MouseButton, + MouseEvent, + MouseEventType, + MouseModifier, +) +from prompt_toolkit.utils import is_windows + +from ..key_bindings import KeyBindings + +if TYPE_CHECKING: + from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone + +__all__ = [ + "load_mouse_bindings", +] + +E = KeyPressEvent + +# fmt: off +# flake8: noqa E201 +SCROLL_UP = MouseEventType.SCROLL_UP +SCROLL_DOWN = MouseEventType.SCROLL_DOWN +MOUSE_DOWN = MouseEventType.MOUSE_DOWN +MOUSE_MOVE = MouseEventType.MOUSE_MOVE +MOUSE_UP = MouseEventType.MOUSE_UP + +NO_MODIFIER : FrozenSet[MouseModifier] = frozenset() +SHIFT : FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT}) +ALT : FrozenSet[MouseModifier] = frozenset({MouseModifier.ALT}) +SHIFT_ALT : FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT, MouseModifier.ALT}) +CONTROL : FrozenSet[MouseModifier] = frozenset({MouseModifier.CONTROL}) +SHIFT_CONTROL : FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT, MouseModifier.CONTROL}) +ALT_CONTROL : FrozenSet[MouseModifier] = frozenset({MouseModifier.ALT, MouseModifier.CONTROL}) +SHIFT_ALT_CONTROL: FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT, MouseModifier.ALT, MouseModifier.CONTROL}) +UNKNOWN_MODIFIER : FrozenSet[MouseModifier] = frozenset() + +LEFT = MouseButton.LEFT +MIDDLE = MouseButton.MIDDLE +RIGHT = MouseButton.RIGHT +NO_BUTTON = MouseButton.NONE +UNKNOWN_BUTTON = MouseButton.UNKNOWN + +xterm_sgr_mouse_events = { + ( 0, 'm') : (LEFT, MOUSE_UP, NO_MODIFIER), # left_up 0+ + + =0 + ( 4, 'm') : (LEFT, MOUSE_UP, SHIFT), # left_up Shift 0+4+ + =4 + ( 8, 'm') : (LEFT, MOUSE_UP, ALT), # left_up Alt 0+ +8+ =8 + (12, 'm') : (LEFT, MOUSE_UP, SHIFT_ALT), # left_up Shift Alt 0+4+8+ =12 + (16, 'm') : (LEFT, MOUSE_UP, CONTROL), # left_up Control 0+ + +16=16 + (20, 'm') : (LEFT, MOUSE_UP, SHIFT_CONTROL), # left_up Shift Control 0+4+ +16=20 + (24, 'm') : (LEFT, MOUSE_UP, ALT_CONTROL), # left_up Alt Control 0+ +8+16=24 + (28, 'm') : (LEFT, MOUSE_UP, SHIFT_ALT_CONTROL), # left_up Shift Alt Control 0+4+8+16=28 + + ( 1, 'm') : (MIDDLE, MOUSE_UP, NO_MODIFIER), # middle_up 1+ + + =1 + ( 5, 'm') : (MIDDLE, MOUSE_UP, SHIFT), # middle_up Shift 1+4+ + =5 + ( 9, 'm') : (MIDDLE, MOUSE_UP, ALT), # middle_up Alt 1+ +8+ =9 + (13, 'm') : (MIDDLE, MOUSE_UP, SHIFT_ALT), # middle_up Shift Alt 1+4+8+ =13 + (17, 'm') : (MIDDLE, MOUSE_UP, CONTROL), # middle_up Control 1+ + +16=17 + (21, 'm') : (MIDDLE, MOUSE_UP, SHIFT_CONTROL), # middle_up Shift Control 1+4+ +16=21 + (25, 'm') : (MIDDLE, MOUSE_UP, ALT_CONTROL), # middle_up Alt Control 1+ +8+16=25 + (29, 'm') : (MIDDLE, MOUSE_UP, SHIFT_ALT_CONTROL), # middle_up Shift Alt Control 1+4+8+16=29 + + ( 2, 'm') : (RIGHT, MOUSE_UP, NO_MODIFIER), # right_up 2+ + + =2 + ( 6, 'm') : (RIGHT, MOUSE_UP, SHIFT), # right_up Shift 2+4+ + =6 + (10, 'm') : (RIGHT, MOUSE_UP, ALT), # right_up Alt 2+ +8+ =10 + (14, 'm') : (RIGHT, MOUSE_UP, SHIFT_ALT), # right_up Shift Alt 2+4+8+ =14 + (18, 'm') : (RIGHT, MOUSE_UP, CONTROL), # right_up Control 2+ + +16=18 + (22, 'm') : (RIGHT, MOUSE_UP, SHIFT_CONTROL), # right_up Shift Control 2+4+ +16=22 + (26, 'm') : (RIGHT, MOUSE_UP, ALT_CONTROL), # right_up Alt Control 2+ +8+16=26 + (30, 'm') : (RIGHT, MOUSE_UP, SHIFT_ALT_CONTROL), # right_up Shift Alt Control 2+4+8+16=30 + + ( 0, 'M') : (LEFT, MOUSE_DOWN, NO_MODIFIER), # left_down 0+ + + =0 + ( 4, 'M') : (LEFT, MOUSE_DOWN, SHIFT), # left_down Shift 0+4+ + =4 + ( 8, 'M') : (LEFT, MOUSE_DOWN, ALT), # left_down Alt 0+ +8+ =8 + (12, 'M') : (LEFT, MOUSE_DOWN, SHIFT_ALT), # left_down Shift Alt 0+4+8+ =12 + (16, 'M') : (LEFT, MOUSE_DOWN, CONTROL), # left_down Control 0+ + +16=16 + (20, 'M') : (LEFT, MOUSE_DOWN, SHIFT_CONTROL), # left_down Shift Control 0+4+ +16=20 + (24, 'M') : (LEFT, MOUSE_DOWN, ALT_CONTROL), # left_down Alt Control 0+ +8+16=24 + (28, 'M') : (LEFT, MOUSE_DOWN, SHIFT_ALT_CONTROL), # left_down Shift Alt Control 0+4+8+16=28 + + ( 1, 'M') : (MIDDLE, MOUSE_DOWN, NO_MODIFIER), # middle_down 1+ + + =1 + ( 5, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT), # middle_down Shift 1+4+ + =5 + ( 9, 'M') : (MIDDLE, MOUSE_DOWN, ALT), # middle_down Alt 1+ +8+ =9 + (13, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT_ALT), # middle_down Shift Alt 1+4+8+ =13 + (17, 'M') : (MIDDLE, MOUSE_DOWN, CONTROL), # middle_down Control 1+ + +16=17 + (21, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT_CONTROL), # middle_down Shift Control 1+4+ +16=21 + (25, 'M') : (MIDDLE, MOUSE_DOWN, ALT_CONTROL), # middle_down Alt Control 1+ +8+16=25 + (29, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT_ALT_CONTROL), # middle_down Shift Alt Control 1+4+8+16=29 + + ( 2, 'M') : (RIGHT, MOUSE_DOWN, NO_MODIFIER), # right_down 2+ + + =2 + ( 6, 'M') : (RIGHT, MOUSE_DOWN, SHIFT), # right_down Shift 2+4+ + =6 + (10, 'M') : (RIGHT, MOUSE_DOWN, ALT), # right_down Alt 2+ +8+ =10 + (14, 'M') : (RIGHT, MOUSE_DOWN, SHIFT_ALT), # right_down Shift Alt 2+4+8+ =14 + (18, 'M') : (RIGHT, MOUSE_DOWN, CONTROL), # right_down Control 2+ + +16=18 + (22, 'M') : (RIGHT, MOUSE_DOWN, SHIFT_CONTROL), # right_down Shift Control 2+4+ +16=22 + (26, 'M') : (RIGHT, MOUSE_DOWN, ALT_CONTROL), # right_down Alt Control 2+ +8+16=26 + (30, 'M') : (RIGHT, MOUSE_DOWN, SHIFT_ALT_CONTROL), # right_down Shift Alt Control 2+4+8+16=30 + + (32, 'M') : (LEFT, MOUSE_MOVE, NO_MODIFIER), # left_drag 32+ + + =32 + (36, 'M') : (LEFT, MOUSE_MOVE, SHIFT), # left_drag Shift 32+4+ + =36 + (40, 'M') : (LEFT, MOUSE_MOVE, ALT), # left_drag Alt 32+ +8+ =40 + (44, 'M') : (LEFT, MOUSE_MOVE, SHIFT_ALT), # left_drag Shift Alt 32+4+8+ =44 + (48, 'M') : (LEFT, MOUSE_MOVE, CONTROL), # left_drag Control 32+ + +16=48 + (52, 'M') : (LEFT, MOUSE_MOVE, SHIFT_CONTROL), # left_drag Shift Control 32+4+ +16=52 + (56, 'M') : (LEFT, MOUSE_MOVE, ALT_CONTROL), # left_drag Alt Control 32+ +8+16=56 + (60, 'M') : (LEFT, MOUSE_MOVE, SHIFT_ALT_CONTROL), # left_drag Shift Alt Control 32+4+8+16=60 + + (33, 'M') : (MIDDLE, MOUSE_MOVE, NO_MODIFIER), # middle_drag 33+ + + =33 + (37, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT), # middle_drag Shift 33+4+ + =37 + (41, 'M') : (MIDDLE, MOUSE_MOVE, ALT), # middle_drag Alt 33+ +8+ =41 + (45, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT_ALT), # middle_drag Shift Alt 33+4+8+ =45 + (49, 'M') : (MIDDLE, MOUSE_MOVE, CONTROL), # middle_drag Control 33+ + +16=49 + (53, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT_CONTROL), # middle_drag Shift Control 33+4+ +16=53 + (57, 'M') : (MIDDLE, MOUSE_MOVE, ALT_CONTROL), # middle_drag Alt Control 33+ +8+16=57 + (61, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT_ALT_CONTROL), # middle_drag Shift Alt Control 33+4+8+16=61 + + (34, 'M') : (RIGHT, MOUSE_MOVE, NO_MODIFIER), # right_drag 34+ + + =34 + (38, 'M') : (RIGHT, MOUSE_MOVE, SHIFT), # right_drag Shift 34+4+ + =38 + (42, 'M') : (RIGHT, MOUSE_MOVE, ALT), # right_drag Alt 34+ +8+ =42 + (46, 'M') : (RIGHT, MOUSE_MOVE, SHIFT_ALT), # right_drag Shift Alt 34+4+8+ =46 + (50, 'M') : (RIGHT, MOUSE_MOVE, CONTROL), # right_drag Control 34+ + +16=50 + (54, 'M') : (RIGHT, MOUSE_MOVE, SHIFT_CONTROL), # right_drag Shift Control 34+4+ +16=54 + (58, 'M') : (RIGHT, MOUSE_MOVE, ALT_CONTROL), # right_drag Alt Control 34+ +8+16=58 + (62, 'M') : (RIGHT, MOUSE_MOVE, SHIFT_ALT_CONTROL), # right_drag Shift Alt Control 34+4+8+16=62 + + (35, 'M') : (NO_BUTTON, MOUSE_MOVE, NO_MODIFIER), # none_drag 35+ + + =35 + (39, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT), # none_drag Shift 35+4+ + =39 + (43, 'M') : (NO_BUTTON, MOUSE_MOVE, ALT), # none_drag Alt 35+ +8+ =43 + (47, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT_ALT), # none_drag Shift Alt 35+4+8+ =47 + (51, 'M') : (NO_BUTTON, MOUSE_MOVE, CONTROL), # none_drag Control 35+ + +16=51 + (55, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT_CONTROL), # none_drag Shift Control 35+4+ +16=55 + (59, 'M') : (NO_BUTTON, MOUSE_MOVE, ALT_CONTROL), # none_drag Alt Control 35+ +8+16=59 + (63, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT_ALT_CONTROL), # none_drag Shift Alt Control 35+4+8+16=63 + + (64, 'M') : (NO_BUTTON, SCROLL_UP, NO_MODIFIER), # scroll_up 64+ + + =64 + (68, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT), # scroll_up Shift 64+4+ + =68 + (72, 'M') : (NO_BUTTON, SCROLL_UP, ALT), # scroll_up Alt 64+ +8+ =72 + (76, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT_ALT), # scroll_up Shift Alt 64+4+8+ =76 + (80, 'M') : (NO_BUTTON, SCROLL_UP, CONTROL), # scroll_up Control 64+ + +16=80 + (84, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT_CONTROL), # scroll_up Shift Control 64+4+ +16=84 + (88, 'M') : (NO_BUTTON, SCROLL_UP, ALT_CONTROL), # scroll_up Alt Control 64+ +8+16=88 + (92, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT_ALT_CONTROL), # scroll_up Shift Alt Control 64+4+8+16=92 + + (65, 'M') : (NO_BUTTON, SCROLL_DOWN, NO_MODIFIER), # scroll_down 64+ + + =65 + (69, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT), # scroll_down Shift 64+4+ + =69 + (73, 'M') : (NO_BUTTON, SCROLL_DOWN, ALT), # scroll_down Alt 64+ +8+ =73 + (77, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT_ALT), # scroll_down Shift Alt 64+4+8+ =77 + (81, 'M') : (NO_BUTTON, SCROLL_DOWN, CONTROL), # scroll_down Control 64+ + +16=81 + (85, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT_CONTROL), # scroll_down Shift Control 64+4+ +16=85 + (89, 'M') : (NO_BUTTON, SCROLL_DOWN, ALT_CONTROL), # scroll_down Alt Control 64+ +8+16=89 + (93, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT_ALT_CONTROL), # scroll_down Shift Alt Control 64+4+8+16=93 +} + +typical_mouse_events = { + 32: (LEFT , MOUSE_DOWN , UNKNOWN_MODIFIER), + 33: (MIDDLE , MOUSE_DOWN , UNKNOWN_MODIFIER), + 34: (RIGHT , MOUSE_DOWN , UNKNOWN_MODIFIER), + 35: (UNKNOWN_BUTTON , MOUSE_UP , UNKNOWN_MODIFIER), + + 64: (LEFT , MOUSE_MOVE , UNKNOWN_MODIFIER), + 65: (MIDDLE , MOUSE_MOVE , UNKNOWN_MODIFIER), + 66: (RIGHT , MOUSE_MOVE , UNKNOWN_MODIFIER), + 67: (NO_BUTTON , MOUSE_MOVE , UNKNOWN_MODIFIER), + + 96: (NO_BUTTON , SCROLL_UP , UNKNOWN_MODIFIER), + 97: (NO_BUTTON , SCROLL_DOWN, UNKNOWN_MODIFIER), +} + +urxvt_mouse_events={ + 32: (UNKNOWN_BUTTON, MOUSE_DOWN , UNKNOWN_MODIFIER), + 35: (UNKNOWN_BUTTON, MOUSE_UP , UNKNOWN_MODIFIER), + 96: (NO_BUTTON , SCROLL_UP , UNKNOWN_MODIFIER), + 97: (NO_BUTTON , SCROLL_DOWN, UNKNOWN_MODIFIER), +} +# fmt:on + + +def load_mouse_bindings() -> KeyBindings: + """ + Key bindings, required for mouse support. + (Mouse events enter through the key binding system.) + """ + key_bindings = KeyBindings() + + @key_bindings.add(Keys.Vt100MouseEvent) + def _(event: E) -> "NotImplementedOrNone": + """ + Handling of incoming mouse event. + """ + # TypicaL: "eSC[MaB*" + # Urxvt: "Esc[96;14;13M" + # Xterm SGR: "Esc[<64;85;12M" + + # Parse incoming packet. + if event.data[2] == "M": + # Typical. + mouse_event, x, y = map(ord, event.data[3:]) + + # TODO: Is it possible to add modifiers here? + mouse_button, mouse_event_type, mouse_modifier = typical_mouse_events[ + mouse_event + ] + + # Handle situations where `PosixStdinReader` used surrogateescapes. + if x >= 0xDC00: + x -= 0xDC00 + if y >= 0xDC00: + y -= 0xDC00 + + x -= 32 + y -= 32 + else: + # Urxvt and Xterm SGR. + # When the '<' is not present, we are not using the Xterm SGR mode, + # but Urxvt instead. + data = event.data[2:] + if data[:1] == "<": + sgr = True + data = data[1:] + else: + sgr = False + + # Extract coordinates. + mouse_event, x, y = map(int, data[:-1].split(";")) + m = data[-1] + + # Parse event type. + if sgr: + try: + ( + mouse_button, + mouse_event_type, + mouse_modifiers, + ) = xterm_sgr_mouse_events[mouse_event, m] + except KeyError: + return NotImplemented + + else: + # Some other terminals, like urxvt, Hyper terminal, ... + ( + mouse_button, + mouse_event_type, + mouse_modifiers, + ) = urxvt_mouse_events.get( + mouse_event, (UNKNOWN_BUTTON, MOUSE_MOVE, UNKNOWN_MODIFIER) + ) + + x -= 1 + y -= 1 + + # Only handle mouse events when we know the window height. + if event.app.renderer.height_is_known and mouse_event_type is not None: + # Take region above the layout into account. The reported + # coordinates are absolute to the visible part of the terminal. + from prompt_toolkit.renderer import HeightIsUnknownError + + try: + y -= event.app.renderer.rows_above_layout + except HeightIsUnknownError: + return NotImplemented + + # Call the mouse handler from the renderer. + + # Note: This can return `NotImplemented` if no mouse handler was + # found for this position, or if no repainting needs to + # happen. this way, we avoid excessive repaints during mouse + # movements. + handler = event.app.renderer.mouse_handlers.mouse_handlers[y][x] + return handler( + MouseEvent( + position=Point(x=x, y=y), + event_type=mouse_event_type, + button=mouse_button, + modifiers=mouse_modifiers, + ) + ) + + return NotImplemented + + @key_bindings.add(Keys.ScrollUp) + def _scroll_up(event: E) -> None: + """ + Scroll up event without cursor position. + """ + # We don't receive a cursor position, so we don't know which window to + # scroll. Just send an 'up' key press instead. + event.key_processor.feed(KeyPress(Keys.Up), first=True) + + @key_bindings.add(Keys.ScrollDown) + def _scroll_down(event: E) -> None: + """ + Scroll down event without cursor position. + """ + event.key_processor.feed(KeyPress(Keys.Down), first=True) + + @key_bindings.add(Keys.WindowsMouseEvent) + def _mouse(event: E) -> "NotImplementedOrNone": + """ + Handling of mouse events for Windows. + """ + assert is_windows() # This key binding should only exist for Windows. + + # Parse data. + pieces = event.data.split(";") + + button = MouseButton(pieces[0]) + event_type = MouseEventType(pieces[1]) + x = int(pieces[2]) + y = int(pieces[3]) + + # Make coordinates absolute to the visible part of the terminal. + output = event.app.renderer.output + + from prompt_toolkit.output.win32 import Win32Output + from prompt_toolkit.output.windows10 import Windows10_Output + + if isinstance(output, (Win32Output, Windows10_Output)): + screen_buffer_info = output.get_win32_screen_buffer_info() + rows_above_cursor = ( + screen_buffer_info.dwCursorPosition.Y - event.app.renderer._cursor_pos.y + ) + y -= rows_above_cursor + + # Call the mouse event handler. + # (Can return `NotImplemented`.) + handler = event.app.renderer.mouse_handlers.mouse_handlers[y][x] + + return handler( + MouseEvent( + position=Point(x=x, y=y), + event_type=event_type, + button=button, + modifiers=UNKNOWN_MODIFIER, + ) + ) + + # No mouse handler found. Return `NotImplemented` so that we don't + # invalidate the UI. + return NotImplemented + + return key_bindings diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/named_commands.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/named_commands.py index 488420185f..e0796ef0b8 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/named_commands.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/named_commands.py @@ -1,687 +1,687 @@ -""" -Key bindings which are also known by GNU Readline by the given names. - -See: http://www.delorie.com/gnu/docs/readline/rlman_13.html -""" -from typing import Callable, Dict, TypeVar, Union, cast - -from prompt_toolkit.document import Document -from prompt_toolkit.enums import EditingMode -from prompt_toolkit.key_binding.key_bindings import Binding, key_binding -from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent -from prompt_toolkit.keys import Keys -from prompt_toolkit.layout.controls import BufferControl -from prompt_toolkit.search import SearchDirection -from prompt_toolkit.selection import PasteMode - -from .completion import display_completions_like_readline, generate_completions - -__all__ = [ - "get_by_name", -] - - -# Typing. -_Handler = Callable[[KeyPressEvent], None] -_HandlerOrBinding = Union[_Handler, Binding] -_T = TypeVar("_T", bound=_HandlerOrBinding) -E = KeyPressEvent - - -# Registry that maps the Readline command names to their handlers. -_readline_commands: Dict[str, Binding] = {} - - -def register(name: str) -> Callable[[_T], _T]: - """ - Store handler in the `_readline_commands` dictionary. - """ - - def decorator(handler: _T) -> _T: - "`handler` is a callable or Binding." - if isinstance(handler, Binding): - _readline_commands[name] = handler - else: - _readline_commands[name] = key_binding()(cast(_Handler, handler)) - - return handler - - return decorator - - -def get_by_name(name: str) -> Binding: - """ - Return the handler for the (Readline) command with the given name. - """ - try: - return _readline_commands[name] - except KeyError as e: - raise KeyError("Unknown Readline command: %r" % name) from e - - -# -# Commands for moving -# See: http://www.delorie.com/gnu/docs/readline/rlman_14.html -# - - -@register("beginning-of-buffer") -def beginning_of_buffer(event: E) -> None: - """ - Move to the start of the buffer. - """ - buff = event.current_buffer - buff.cursor_position = 0 - - -@register("end-of-buffer") -def end_of_buffer(event: E) -> None: - """ - Move to the end of the buffer. - """ - buff = event.current_buffer - buff.cursor_position = len(buff.text) - - -@register("beginning-of-line") -def beginning_of_line(event: E) -> None: - """ - Move to the start of the current line. - """ - buff = event.current_buffer - buff.cursor_position += buff.document.get_start_of_line_position( - after_whitespace=False - ) - - -@register("end-of-line") -def end_of_line(event: E) -> None: - """ - Move to the end of the line. - """ - buff = event.current_buffer - buff.cursor_position += buff.document.get_end_of_line_position() - - -@register("forward-char") -def forward_char(event: E) -> None: - """ - Move forward a character. - """ - buff = event.current_buffer - buff.cursor_position += buff.document.get_cursor_right_position(count=event.arg) - - -@register("backward-char") -def backward_char(event: E) -> None: - "Move back a character." - buff = event.current_buffer - buff.cursor_position += buff.document.get_cursor_left_position(count=event.arg) - - -@register("forward-word") -def forward_word(event: E) -> None: - """ - Move forward to the end of the next word. Words are composed of letters and - digits. - """ - buff = event.current_buffer - pos = buff.document.find_next_word_ending(count=event.arg) - - if pos: - buff.cursor_position += pos - - -@register("backward-word") -def backward_word(event: E) -> None: - """ - Move back to the start of the current or previous word. Words are composed - of letters and digits. - """ - buff = event.current_buffer - pos = buff.document.find_previous_word_beginning(count=event.arg) - - if pos: - buff.cursor_position += pos - - -@register("clear-screen") -def clear_screen(event: E) -> None: - """ - Clear the screen and redraw everything at the top of the screen. - """ - event.app.renderer.clear() - - -@register("redraw-current-line") -def redraw_current_line(event: E) -> None: - """ - Refresh the current line. - (Readline defines this command, but prompt-toolkit doesn't have it.) - """ - pass - - -# -# Commands for manipulating the history. -# See: http://www.delorie.com/gnu/docs/readline/rlman_15.html -# - - -@register("accept-line") -def accept_line(event: E) -> None: - """ - Accept the line regardless of where the cursor is. - """ - event.current_buffer.validate_and_handle() - - -@register("previous-history") -def previous_history(event: E) -> None: - """ - Move `back` through the history list, fetching the previous command. - """ - event.current_buffer.history_backward(count=event.arg) - - -@register("next-history") -def next_history(event: E) -> None: - """ - Move `forward` through the history list, fetching the next command. - """ - event.current_buffer.history_forward(count=event.arg) - - -@register("beginning-of-history") -def beginning_of_history(event: E) -> None: - """ - Move to the first line in the history. - """ - event.current_buffer.go_to_history(0) - - -@register("end-of-history") -def end_of_history(event: E) -> None: - """ - Move to the end of the input history, i.e., the line currently being entered. - """ +""" +Key bindings which are also known by GNU Readline by the given names. + +See: http://www.delorie.com/gnu/docs/readline/rlman_13.html +""" +from typing import Callable, Dict, TypeVar, Union, cast + +from prompt_toolkit.document import Document +from prompt_toolkit.enums import EditingMode +from prompt_toolkit.key_binding.key_bindings import Binding, key_binding +from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent +from prompt_toolkit.keys import Keys +from prompt_toolkit.layout.controls import BufferControl +from prompt_toolkit.search import SearchDirection +from prompt_toolkit.selection import PasteMode + +from .completion import display_completions_like_readline, generate_completions + +__all__ = [ + "get_by_name", +] + + +# Typing. +_Handler = Callable[[KeyPressEvent], None] +_HandlerOrBinding = Union[_Handler, Binding] +_T = TypeVar("_T", bound=_HandlerOrBinding) +E = KeyPressEvent + + +# Registry that maps the Readline command names to their handlers. +_readline_commands: Dict[str, Binding] = {} + + +def register(name: str) -> Callable[[_T], _T]: + """ + Store handler in the `_readline_commands` dictionary. + """ + + def decorator(handler: _T) -> _T: + "`handler` is a callable or Binding." + if isinstance(handler, Binding): + _readline_commands[name] = handler + else: + _readline_commands[name] = key_binding()(cast(_Handler, handler)) + + return handler + + return decorator + + +def get_by_name(name: str) -> Binding: + """ + Return the handler for the (Readline) command with the given name. + """ + try: + return _readline_commands[name] + except KeyError as e: + raise KeyError("Unknown Readline command: %r" % name) from e + + +# +# Commands for moving +# See: http://www.delorie.com/gnu/docs/readline/rlman_14.html +# + + +@register("beginning-of-buffer") +def beginning_of_buffer(event: E) -> None: + """ + Move to the start of the buffer. + """ + buff = event.current_buffer + buff.cursor_position = 0 + + +@register("end-of-buffer") +def end_of_buffer(event: E) -> None: + """ + Move to the end of the buffer. + """ + buff = event.current_buffer + buff.cursor_position = len(buff.text) + + +@register("beginning-of-line") +def beginning_of_line(event: E) -> None: + """ + Move to the start of the current line. + """ + buff = event.current_buffer + buff.cursor_position += buff.document.get_start_of_line_position( + after_whitespace=False + ) + + +@register("end-of-line") +def end_of_line(event: E) -> None: + """ + Move to the end of the line. + """ + buff = event.current_buffer + buff.cursor_position += buff.document.get_end_of_line_position() + + +@register("forward-char") +def forward_char(event: E) -> None: + """ + Move forward a character. + """ + buff = event.current_buffer + buff.cursor_position += buff.document.get_cursor_right_position(count=event.arg) + + +@register("backward-char") +def backward_char(event: E) -> None: + "Move back a character." + buff = event.current_buffer + buff.cursor_position += buff.document.get_cursor_left_position(count=event.arg) + + +@register("forward-word") +def forward_word(event: E) -> None: + """ + Move forward to the end of the next word. Words are composed of letters and + digits. + """ + buff = event.current_buffer + pos = buff.document.find_next_word_ending(count=event.arg) + + if pos: + buff.cursor_position += pos + + +@register("backward-word") +def backward_word(event: E) -> None: + """ + Move back to the start of the current or previous word. Words are composed + of letters and digits. + """ + buff = event.current_buffer + pos = buff.document.find_previous_word_beginning(count=event.arg) + + if pos: + buff.cursor_position += pos + + +@register("clear-screen") +def clear_screen(event: E) -> None: + """ + Clear the screen and redraw everything at the top of the screen. + """ + event.app.renderer.clear() + + +@register("redraw-current-line") +def redraw_current_line(event: E) -> None: + """ + Refresh the current line. + (Readline defines this command, but prompt-toolkit doesn't have it.) + """ + pass + + +# +# Commands for manipulating the history. +# See: http://www.delorie.com/gnu/docs/readline/rlman_15.html +# + + +@register("accept-line") +def accept_line(event: E) -> None: + """ + Accept the line regardless of where the cursor is. + """ + event.current_buffer.validate_and_handle() + + +@register("previous-history") +def previous_history(event: E) -> None: + """ + Move `back` through the history list, fetching the previous command. + """ + event.current_buffer.history_backward(count=event.arg) + + +@register("next-history") +def next_history(event: E) -> None: + """ + Move `forward` through the history list, fetching the next command. + """ + event.current_buffer.history_forward(count=event.arg) + + +@register("beginning-of-history") +def beginning_of_history(event: E) -> None: + """ + Move to the first line in the history. + """ + event.current_buffer.go_to_history(0) + + +@register("end-of-history") +def end_of_history(event: E) -> None: + """ + Move to the end of the input history, i.e., the line currently being entered. + """ event.current_buffer.history_forward(count=10**100) - buff = event.current_buffer - buff.go_to_history(len(buff._working_lines) - 1) - - -@register("reverse-search-history") -def reverse_search_history(event: E) -> None: - """ - Search backward starting at the current line and moving `up` through - the history as necessary. This is an incremental search. - """ - control = event.app.layout.current_control - - if isinstance(control, BufferControl) and control.search_buffer_control: - event.app.current_search_state.direction = SearchDirection.BACKWARD - event.app.layout.current_control = control.search_buffer_control - - -# -# Commands for changing text -# - - -@register("end-of-file") -def end_of_file(event: E) -> None: - """ - Exit. - """ - event.app.exit() - - -@register("delete-char") -def delete_char(event: E) -> None: - """ - Delete character before the cursor. - """ - deleted = event.current_buffer.delete(count=event.arg) - if not deleted: - event.app.output.bell() - - -@register("backward-delete-char") -def backward_delete_char(event: E) -> None: - """ - Delete the character behind the cursor. - """ - if event.arg < 0: - # When a negative argument has been given, this should delete in front - # of the cursor. - deleted = event.current_buffer.delete(count=-event.arg) - else: - deleted = event.current_buffer.delete_before_cursor(count=event.arg) - - if not deleted: - event.app.output.bell() - - -@register("self-insert") -def self_insert(event: E) -> None: - """ - Insert yourself. - """ - event.current_buffer.insert_text(event.data * event.arg) - - -@register("transpose-chars") -def transpose_chars(event: E) -> None: - """ - Emulate Emacs transpose-char behavior: at the beginning of the buffer, - do nothing. At the end of a line or buffer, swap the characters before - the cursor. Otherwise, move the cursor right, and then swap the - characters before the cursor. - """ - b = event.current_buffer - p = b.cursor_position - if p == 0: - return - elif p == len(b.text) or b.text[p] == "\n": - b.swap_characters_before_cursor() - else: - b.cursor_position += b.document.get_cursor_right_position() - b.swap_characters_before_cursor() - - -@register("uppercase-word") -def uppercase_word(event: E) -> None: - """ - Uppercase the current (or following) word. - """ - buff = event.current_buffer - - for i in range(event.arg): - pos = buff.document.find_next_word_ending() - words = buff.document.text_after_cursor[:pos] - buff.insert_text(words.upper(), overwrite=True) - - -@register("downcase-word") -def downcase_word(event: E) -> None: - """ - Lowercase the current (or following) word. - """ - buff = event.current_buffer - - for i in range(event.arg): # XXX: not DRY: see meta_c and meta_u!! - pos = buff.document.find_next_word_ending() - words = buff.document.text_after_cursor[:pos] - buff.insert_text(words.lower(), overwrite=True) - - -@register("capitalize-word") -def capitalize_word(event: E) -> None: - """ - Capitalize the current (or following) word. - """ - buff = event.current_buffer - - for i in range(event.arg): - pos = buff.document.find_next_word_ending() - words = buff.document.text_after_cursor[:pos] - buff.insert_text(words.title(), overwrite=True) - - -@register("quoted-insert") -def quoted_insert(event: E) -> None: - """ - Add the next character typed to the line verbatim. This is how to insert - key sequences like C-q, for example. - """ - event.app.quoted_insert = True - - -# -# Killing and yanking. -# - - -@register("kill-line") -def kill_line(event: E) -> None: - """ - Kill the text from the cursor to the end of the line. - - If we are at the end of the line, this should remove the newline. - (That way, it is possible to delete multiple lines by executing this - command multiple times.) - """ - buff = event.current_buffer - if event.arg < 0: - deleted = buff.delete_before_cursor( - count=-buff.document.get_start_of_line_position() - ) - else: - if buff.document.current_char == "\n": - deleted = buff.delete(1) - else: - deleted = buff.delete(count=buff.document.get_end_of_line_position()) - event.app.clipboard.set_text(deleted) - - -@register("kill-word") -def kill_word(event: E) -> None: - """ - Kill from point to the end of the current word, or if between words, to the - end of the next word. Word boundaries are the same as forward-word. - """ - buff = event.current_buffer - pos = buff.document.find_next_word_ending(count=event.arg) - - if pos: - deleted = buff.delete(count=pos) - - if event.is_repeat: - deleted = event.app.clipboard.get_data().text + deleted - - event.app.clipboard.set_text(deleted) - - -@register("unix-word-rubout") -def unix_word_rubout(event: E, WORD: bool = True) -> None: - """ - Kill the word behind point, using whitespace as a word boundary. - Usually bound to ControlW. - """ - buff = event.current_buffer - pos = buff.document.find_start_of_previous_word(count=event.arg, WORD=WORD) - - if pos is None: - # Nothing found? delete until the start of the document. (The - # input starts with whitespace and no words were found before the - # cursor.) - pos = -buff.cursor_position - - if pos: - deleted = buff.delete_before_cursor(count=-pos) - - # If the previous key press was also Control-W, concatenate deleted - # text. - if event.is_repeat: - deleted += event.app.clipboard.get_data().text - - event.app.clipboard.set_text(deleted) - else: - # Nothing to delete. Bell. - event.app.output.bell() - - -@register("backward-kill-word") -def backward_kill_word(event: E) -> None: - """ - Kills the word before point, using "not a letter nor a digit" as a word boundary. - Usually bound to M-Del or M-Backspace. - """ - unix_word_rubout(event, WORD=False) - - -@register("delete-horizontal-space") -def delete_horizontal_space(event: E) -> None: - """ - Delete all spaces and tabs around point. - """ - buff = event.current_buffer - text_before_cursor = buff.document.text_before_cursor - text_after_cursor = buff.document.text_after_cursor - - delete_before = len(text_before_cursor) - len(text_before_cursor.rstrip("\t ")) - delete_after = len(text_after_cursor) - len(text_after_cursor.lstrip("\t ")) - - buff.delete_before_cursor(count=delete_before) - buff.delete(count=delete_after) - - -@register("unix-line-discard") -def unix_line_discard(event: E) -> None: - """ - Kill backward from the cursor to the beginning of the current line. - """ - buff = event.current_buffer - - if buff.document.cursor_position_col == 0 and buff.document.cursor_position > 0: - buff.delete_before_cursor(count=1) - else: - deleted = buff.delete_before_cursor( - count=-buff.document.get_start_of_line_position() - ) - event.app.clipboard.set_text(deleted) - - -@register("yank") -def yank(event: E) -> None: - """ - Paste before cursor. - """ - event.current_buffer.paste_clipboard_data( - event.app.clipboard.get_data(), count=event.arg, paste_mode=PasteMode.EMACS - ) - - -@register("yank-nth-arg") -def yank_nth_arg(event: E) -> None: - """ - Insert the first argument of the previous command. With an argument, insert - the nth word from the previous command (start counting at 0). - """ - n = event.arg if event.arg_present else None - event.current_buffer.yank_nth_arg(n) - - -@register("yank-last-arg") -def yank_last_arg(event: E) -> None: - """ - Like `yank_nth_arg`, but if no argument has been given, yank the last word - of each line. - """ - n = event.arg if event.arg_present else None - event.current_buffer.yank_last_arg(n) - - -@register("yank-pop") -def yank_pop(event: E) -> None: - """ - Rotate the kill ring, and yank the new top. Only works following yank or - yank-pop. - """ - buff = event.current_buffer - doc_before_paste = buff.document_before_paste - clipboard = event.app.clipboard - - if doc_before_paste is not None: - buff.document = doc_before_paste - clipboard.rotate() - buff.paste_clipboard_data(clipboard.get_data(), paste_mode=PasteMode.EMACS) - - -# -# Completion. -# - - -@register("complete") -def complete(event: E) -> None: - """ - Attempt to perform completion. - """ - display_completions_like_readline(event) - - -@register("menu-complete") -def menu_complete(event: E) -> None: - """ - Generate completions, or go to the next completion. (This is the default - way of completing input in prompt_toolkit.) - """ - generate_completions(event) - - -@register("menu-complete-backward") -def menu_complete_backward(event: E) -> None: - """ - Move backward through the list of possible completions. - """ - event.current_buffer.complete_previous() - - -# -# Keyboard macros. -# - - -@register("start-kbd-macro") -def start_kbd_macro(event: E) -> None: - """ - Begin saving the characters typed into the current keyboard macro. - """ - event.app.emacs_state.start_macro() - - -@register("end-kbd-macro") -def end_kbd_macro(event: E) -> None: - """ - Stop saving the characters typed into the current keyboard macro and save - the definition. - """ - event.app.emacs_state.end_macro() - - -@register("call-last-kbd-macro") -@key_binding(record_in_macro=False) -def call_last_kbd_macro(event: E) -> None: - """ - Re-execute the last keyboard macro defined, by making the characters in the - macro appear as if typed at the keyboard. - - Notice that we pass `record_in_macro=False`. This ensures that the 'c-x e' - key sequence doesn't appear in the recording itself. This function inserts - the body of the called macro back into the KeyProcessor, so these keys will - be added later on to the macro of their handlers have `record_in_macro=True`. - """ - # Insert the macro. - macro = event.app.emacs_state.macro - - if macro: - event.app.key_processor.feed_multiple(macro, first=True) - - -@register("print-last-kbd-macro") -def print_last_kbd_macro(event: E) -> None: - """ - Print the last keyboard macro. - """ - # TODO: Make the format suitable for the inputrc file. - def print_macro() -> None: - macro = event.app.emacs_state.macro - if macro: - for k in macro: - print(k) - - from prompt_toolkit.application.run_in_terminal import run_in_terminal - - run_in_terminal(print_macro) - - -# -# Miscellaneous Commands. -# - - -@register("undo") -def undo(event: E) -> None: - """ - Incremental undo. - """ - event.current_buffer.undo() - - -@register("insert-comment") -def insert_comment(event: E) -> None: - """ - Without numeric argument, comment all lines. - With numeric argument, uncomment all lines. - In any case accept the input. - """ - buff = event.current_buffer - - # Transform all lines. - if event.arg != 1: - - def change(line: str) -> str: - return line[1:] if line.startswith("#") else line - - else: - - def change(line: str) -> str: - return "#" + line - - buff.document = Document( - text="\n".join(map(change, buff.text.splitlines())), cursor_position=0 - ) - - # Accept input. - buff.validate_and_handle() - - -@register("vi-editing-mode") -def vi_editing_mode(event: E) -> None: - """ - Switch to Vi editing mode. - """ - event.app.editing_mode = EditingMode.VI - - -@register("emacs-editing-mode") -def emacs_editing_mode(event: E) -> None: - """ - Switch to Emacs editing mode. - """ - event.app.editing_mode = EditingMode.EMACS - - -@register("prefix-meta") -def prefix_meta(event: E) -> None: - """ - Metafy the next character typed. This is for keyboards without a meta key. - - Sometimes people also want to bind other keys to Meta, e.g. 'jj':: - - key_bindings.add_key_binding('j', 'j', filter=ViInsertMode())(prefix_meta) - """ - # ('first' should be true, because we want to insert it at the current - # position in the queue.) - event.app.key_processor.feed(KeyPress(Keys.Escape), first=True) - - -@register("operate-and-get-next") -def operate_and_get_next(event: E) -> None: - """ - Accept the current line for execution and fetch the next line relative to - the current line from the history for editing. - """ - buff = event.current_buffer - new_index = buff.working_index + 1 - - # Accept the current input. (This will also redraw the interface in the - # 'done' state.) - buff.validate_and_handle() - - # Set the new index at the start of the next run. - def set_working_index() -> None: - if new_index < len(buff._working_lines): - buff.working_index = new_index - - event.app.pre_run_callables.append(set_working_index) - - -@register("edit-and-execute-command") -def edit_and_execute(event: E) -> None: - """ - Invoke an editor on the current command line, and accept the result. - """ - buff = event.current_buffer - buff.open_in_editor(validate_and_handle=True) + buff = event.current_buffer + buff.go_to_history(len(buff._working_lines) - 1) + + +@register("reverse-search-history") +def reverse_search_history(event: E) -> None: + """ + Search backward starting at the current line and moving `up` through + the history as necessary. This is an incremental search. + """ + control = event.app.layout.current_control + + if isinstance(control, BufferControl) and control.search_buffer_control: + event.app.current_search_state.direction = SearchDirection.BACKWARD + event.app.layout.current_control = control.search_buffer_control + + +# +# Commands for changing text +# + + +@register("end-of-file") +def end_of_file(event: E) -> None: + """ + Exit. + """ + event.app.exit() + + +@register("delete-char") +def delete_char(event: E) -> None: + """ + Delete character before the cursor. + """ + deleted = event.current_buffer.delete(count=event.arg) + if not deleted: + event.app.output.bell() + + +@register("backward-delete-char") +def backward_delete_char(event: E) -> None: + """ + Delete the character behind the cursor. + """ + if event.arg < 0: + # When a negative argument has been given, this should delete in front + # of the cursor. + deleted = event.current_buffer.delete(count=-event.arg) + else: + deleted = event.current_buffer.delete_before_cursor(count=event.arg) + + if not deleted: + event.app.output.bell() + + +@register("self-insert") +def self_insert(event: E) -> None: + """ + Insert yourself. + """ + event.current_buffer.insert_text(event.data * event.arg) + + +@register("transpose-chars") +def transpose_chars(event: E) -> None: + """ + Emulate Emacs transpose-char behavior: at the beginning of the buffer, + do nothing. At the end of a line or buffer, swap the characters before + the cursor. Otherwise, move the cursor right, and then swap the + characters before the cursor. + """ + b = event.current_buffer + p = b.cursor_position + if p == 0: + return + elif p == len(b.text) or b.text[p] == "\n": + b.swap_characters_before_cursor() + else: + b.cursor_position += b.document.get_cursor_right_position() + b.swap_characters_before_cursor() + + +@register("uppercase-word") +def uppercase_word(event: E) -> None: + """ + Uppercase the current (or following) word. + """ + buff = event.current_buffer + + for i in range(event.arg): + pos = buff.document.find_next_word_ending() + words = buff.document.text_after_cursor[:pos] + buff.insert_text(words.upper(), overwrite=True) + + +@register("downcase-word") +def downcase_word(event: E) -> None: + """ + Lowercase the current (or following) word. + """ + buff = event.current_buffer + + for i in range(event.arg): # XXX: not DRY: see meta_c and meta_u!! + pos = buff.document.find_next_word_ending() + words = buff.document.text_after_cursor[:pos] + buff.insert_text(words.lower(), overwrite=True) + + +@register("capitalize-word") +def capitalize_word(event: E) -> None: + """ + Capitalize the current (or following) word. + """ + buff = event.current_buffer + + for i in range(event.arg): + pos = buff.document.find_next_word_ending() + words = buff.document.text_after_cursor[:pos] + buff.insert_text(words.title(), overwrite=True) + + +@register("quoted-insert") +def quoted_insert(event: E) -> None: + """ + Add the next character typed to the line verbatim. This is how to insert + key sequences like C-q, for example. + """ + event.app.quoted_insert = True + + +# +# Killing and yanking. +# + + +@register("kill-line") +def kill_line(event: E) -> None: + """ + Kill the text from the cursor to the end of the line. + + If we are at the end of the line, this should remove the newline. + (That way, it is possible to delete multiple lines by executing this + command multiple times.) + """ + buff = event.current_buffer + if event.arg < 0: + deleted = buff.delete_before_cursor( + count=-buff.document.get_start_of_line_position() + ) + else: + if buff.document.current_char == "\n": + deleted = buff.delete(1) + else: + deleted = buff.delete(count=buff.document.get_end_of_line_position()) + event.app.clipboard.set_text(deleted) + + +@register("kill-word") +def kill_word(event: E) -> None: + """ + Kill from point to the end of the current word, or if between words, to the + end of the next word. Word boundaries are the same as forward-word. + """ + buff = event.current_buffer + pos = buff.document.find_next_word_ending(count=event.arg) + + if pos: + deleted = buff.delete(count=pos) + + if event.is_repeat: + deleted = event.app.clipboard.get_data().text + deleted + + event.app.clipboard.set_text(deleted) + + +@register("unix-word-rubout") +def unix_word_rubout(event: E, WORD: bool = True) -> None: + """ + Kill the word behind point, using whitespace as a word boundary. + Usually bound to ControlW. + """ + buff = event.current_buffer + pos = buff.document.find_start_of_previous_word(count=event.arg, WORD=WORD) + + if pos is None: + # Nothing found? delete until the start of the document. (The + # input starts with whitespace and no words were found before the + # cursor.) + pos = -buff.cursor_position + + if pos: + deleted = buff.delete_before_cursor(count=-pos) + + # If the previous key press was also Control-W, concatenate deleted + # text. + if event.is_repeat: + deleted += event.app.clipboard.get_data().text + + event.app.clipboard.set_text(deleted) + else: + # Nothing to delete. Bell. + event.app.output.bell() + + +@register("backward-kill-word") +def backward_kill_word(event: E) -> None: + """ + Kills the word before point, using "not a letter nor a digit" as a word boundary. + Usually bound to M-Del or M-Backspace. + """ + unix_word_rubout(event, WORD=False) + + +@register("delete-horizontal-space") +def delete_horizontal_space(event: E) -> None: + """ + Delete all spaces and tabs around point. + """ + buff = event.current_buffer + text_before_cursor = buff.document.text_before_cursor + text_after_cursor = buff.document.text_after_cursor + + delete_before = len(text_before_cursor) - len(text_before_cursor.rstrip("\t ")) + delete_after = len(text_after_cursor) - len(text_after_cursor.lstrip("\t ")) + + buff.delete_before_cursor(count=delete_before) + buff.delete(count=delete_after) + + +@register("unix-line-discard") +def unix_line_discard(event: E) -> None: + """ + Kill backward from the cursor to the beginning of the current line. + """ + buff = event.current_buffer + + if buff.document.cursor_position_col == 0 and buff.document.cursor_position > 0: + buff.delete_before_cursor(count=1) + else: + deleted = buff.delete_before_cursor( + count=-buff.document.get_start_of_line_position() + ) + event.app.clipboard.set_text(deleted) + + +@register("yank") +def yank(event: E) -> None: + """ + Paste before cursor. + """ + event.current_buffer.paste_clipboard_data( + event.app.clipboard.get_data(), count=event.arg, paste_mode=PasteMode.EMACS + ) + + +@register("yank-nth-arg") +def yank_nth_arg(event: E) -> None: + """ + Insert the first argument of the previous command. With an argument, insert + the nth word from the previous command (start counting at 0). + """ + n = event.arg if event.arg_present else None + event.current_buffer.yank_nth_arg(n) + + +@register("yank-last-arg") +def yank_last_arg(event: E) -> None: + """ + Like `yank_nth_arg`, but if no argument has been given, yank the last word + of each line. + """ + n = event.arg if event.arg_present else None + event.current_buffer.yank_last_arg(n) + + +@register("yank-pop") +def yank_pop(event: E) -> None: + """ + Rotate the kill ring, and yank the new top. Only works following yank or + yank-pop. + """ + buff = event.current_buffer + doc_before_paste = buff.document_before_paste + clipboard = event.app.clipboard + + if doc_before_paste is not None: + buff.document = doc_before_paste + clipboard.rotate() + buff.paste_clipboard_data(clipboard.get_data(), paste_mode=PasteMode.EMACS) + + +# +# Completion. +# + + +@register("complete") +def complete(event: E) -> None: + """ + Attempt to perform completion. + """ + display_completions_like_readline(event) + + +@register("menu-complete") +def menu_complete(event: E) -> None: + """ + Generate completions, or go to the next completion. (This is the default + way of completing input in prompt_toolkit.) + """ + generate_completions(event) + + +@register("menu-complete-backward") +def menu_complete_backward(event: E) -> None: + """ + Move backward through the list of possible completions. + """ + event.current_buffer.complete_previous() + + +# +# Keyboard macros. +# + + +@register("start-kbd-macro") +def start_kbd_macro(event: E) -> None: + """ + Begin saving the characters typed into the current keyboard macro. + """ + event.app.emacs_state.start_macro() + + +@register("end-kbd-macro") +def end_kbd_macro(event: E) -> None: + """ + Stop saving the characters typed into the current keyboard macro and save + the definition. + """ + event.app.emacs_state.end_macro() + + +@register("call-last-kbd-macro") +@key_binding(record_in_macro=False) +def call_last_kbd_macro(event: E) -> None: + """ + Re-execute the last keyboard macro defined, by making the characters in the + macro appear as if typed at the keyboard. + + Notice that we pass `record_in_macro=False`. This ensures that the 'c-x e' + key sequence doesn't appear in the recording itself. This function inserts + the body of the called macro back into the KeyProcessor, so these keys will + be added later on to the macro of their handlers have `record_in_macro=True`. + """ + # Insert the macro. + macro = event.app.emacs_state.macro + + if macro: + event.app.key_processor.feed_multiple(macro, first=True) + + +@register("print-last-kbd-macro") +def print_last_kbd_macro(event: E) -> None: + """ + Print the last keyboard macro. + """ + # TODO: Make the format suitable for the inputrc file. + def print_macro() -> None: + macro = event.app.emacs_state.macro + if macro: + for k in macro: + print(k) + + from prompt_toolkit.application.run_in_terminal import run_in_terminal + + run_in_terminal(print_macro) + + +# +# Miscellaneous Commands. +# + + +@register("undo") +def undo(event: E) -> None: + """ + Incremental undo. + """ + event.current_buffer.undo() + + +@register("insert-comment") +def insert_comment(event: E) -> None: + """ + Without numeric argument, comment all lines. + With numeric argument, uncomment all lines. + In any case accept the input. + """ + buff = event.current_buffer + + # Transform all lines. + if event.arg != 1: + + def change(line: str) -> str: + return line[1:] if line.startswith("#") else line + + else: + + def change(line: str) -> str: + return "#" + line + + buff.document = Document( + text="\n".join(map(change, buff.text.splitlines())), cursor_position=0 + ) + + # Accept input. + buff.validate_and_handle() + + +@register("vi-editing-mode") +def vi_editing_mode(event: E) -> None: + """ + Switch to Vi editing mode. + """ + event.app.editing_mode = EditingMode.VI + + +@register("emacs-editing-mode") +def emacs_editing_mode(event: E) -> None: + """ + Switch to Emacs editing mode. + """ + event.app.editing_mode = EditingMode.EMACS + + +@register("prefix-meta") +def prefix_meta(event: E) -> None: + """ + Metafy the next character typed. This is for keyboards without a meta key. + + Sometimes people also want to bind other keys to Meta, e.g. 'jj':: + + key_bindings.add_key_binding('j', 'j', filter=ViInsertMode())(prefix_meta) + """ + # ('first' should be true, because we want to insert it at the current + # position in the queue.) + event.app.key_processor.feed(KeyPress(Keys.Escape), first=True) + + +@register("operate-and-get-next") +def operate_and_get_next(event: E) -> None: + """ + Accept the current line for execution and fetch the next line relative to + the current line from the history for editing. + """ + buff = event.current_buffer + new_index = buff.working_index + 1 + + # Accept the current input. (This will also redraw the interface in the + # 'done' state.) + buff.validate_and_handle() + + # Set the new index at the start of the next run. + def set_working_index() -> None: + if new_index < len(buff._working_lines): + buff.working_index = new_index + + event.app.pre_run_callables.append(set_working_index) + + +@register("edit-and-execute-command") +def edit_and_execute(event: E) -> None: + """ + Invoke an editor on the current command line, and accept the result. + """ + buff = event.current_buffer + buff.open_in_editor(validate_and_handle=True) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/open_in_editor.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/open_in_editor.py index 41647be40f..f8699f4a45 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/open_in_editor.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/open_in_editor.py @@ -1,49 +1,49 @@ -""" -Open in editor key bindings. -""" -from prompt_toolkit.filters import emacs_mode, has_selection, vi_navigation_mode - -from ..key_bindings import KeyBindings, KeyBindingsBase, merge_key_bindings -from .named_commands import get_by_name - -__all__ = [ - "load_open_in_editor_bindings", - "load_emacs_open_in_editor_bindings", - "load_vi_open_in_editor_bindings", -] - - -def load_open_in_editor_bindings() -> KeyBindingsBase: - """ - Load both the Vi and emacs key bindings for handling edit-and-execute-command. - """ - return merge_key_bindings( - [ - load_emacs_open_in_editor_bindings(), - load_vi_open_in_editor_bindings(), - ] - ) - - -def load_emacs_open_in_editor_bindings() -> KeyBindings: - """ - Pressing C-X C-E will open the buffer in an external editor. - """ - key_bindings = KeyBindings() - - key_bindings.add("c-x", "c-e", filter=emacs_mode & ~has_selection)( - get_by_name("edit-and-execute-command") - ) - - return key_bindings - - -def load_vi_open_in_editor_bindings() -> KeyBindings: - """ - Pressing 'v' in navigation mode will open the buffer in an external editor. - """ - key_bindings = KeyBindings() - key_bindings.add("v", filter=vi_navigation_mode)( - get_by_name("edit-and-execute-command") - ) - return key_bindings +""" +Open in editor key bindings. +""" +from prompt_toolkit.filters import emacs_mode, has_selection, vi_navigation_mode + +from ..key_bindings import KeyBindings, KeyBindingsBase, merge_key_bindings +from .named_commands import get_by_name + +__all__ = [ + "load_open_in_editor_bindings", + "load_emacs_open_in_editor_bindings", + "load_vi_open_in_editor_bindings", +] + + +def load_open_in_editor_bindings() -> KeyBindingsBase: + """ + Load both the Vi and emacs key bindings for handling edit-and-execute-command. + """ + return merge_key_bindings( + [ + load_emacs_open_in_editor_bindings(), + load_vi_open_in_editor_bindings(), + ] + ) + + +def load_emacs_open_in_editor_bindings() -> KeyBindings: + """ + Pressing C-X C-E will open the buffer in an external editor. + """ + key_bindings = KeyBindings() + + key_bindings.add("c-x", "c-e", filter=emacs_mode & ~has_selection)( + get_by_name("edit-and-execute-command") + ) + + return key_bindings + + +def load_vi_open_in_editor_bindings() -> KeyBindings: + """ + Pressing 'v' in navigation mode will open the buffer in an external editor. + """ + key_bindings = KeyBindings() + key_bindings.add("v", filter=vi_navigation_mode)( + get_by_name("edit-and-execute-command") + ) + return key_bindings diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/page_navigation.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/page_navigation.py index 5a9e4d4668..4d531c0437 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/page_navigation.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/page_navigation.py @@ -1,82 +1,82 @@ -""" -Key bindings for extra page navigation: bindings for up/down scrolling through -long pages, like in Emacs or Vi. -""" -from prompt_toolkit.filters import buffer_has_focus, emacs_mode, vi_mode -from prompt_toolkit.key_binding.key_bindings import ( - ConditionalKeyBindings, - KeyBindings, - KeyBindingsBase, - merge_key_bindings, -) - -from .scroll import ( - scroll_backward, - scroll_forward, - scroll_half_page_down, - scroll_half_page_up, - scroll_one_line_down, - scroll_one_line_up, - scroll_page_down, - scroll_page_up, -) - -__all__ = [ - "load_page_navigation_bindings", - "load_emacs_page_navigation_bindings", - "load_vi_page_navigation_bindings", -] - - -def load_page_navigation_bindings() -> KeyBindingsBase: - """ - Load both the Vi and Emacs bindings for page navigation. - """ - # Only enable when a `Buffer` is focused, otherwise, we would catch keys - # when another widget is focused (like for instance `c-d` in a - # ptterm.Terminal). - return ConditionalKeyBindings( - merge_key_bindings( - [ - load_emacs_page_navigation_bindings(), - load_vi_page_navigation_bindings(), - ] - ), - buffer_has_focus, - ) - - -def load_emacs_page_navigation_bindings() -> KeyBindingsBase: - """ - Key bindings, for scrolling up and down through pages. - This are separate bindings, because GNU readline doesn't have them. - """ - key_bindings = KeyBindings() - handle = key_bindings.add - - handle("c-v")(scroll_page_down) - handle("pagedown")(scroll_page_down) - handle("escape", "v")(scroll_page_up) - handle("pageup")(scroll_page_up) - - return ConditionalKeyBindings(key_bindings, emacs_mode) - - -def load_vi_page_navigation_bindings() -> KeyBindingsBase: - """ - Key bindings, for scrolling up and down through pages. - This are separate bindings, because GNU readline doesn't have them. - """ - key_bindings = KeyBindings() - handle = key_bindings.add - - handle("c-f")(scroll_forward) - handle("c-b")(scroll_backward) - handle("c-d")(scroll_half_page_down) - handle("c-u")(scroll_half_page_up) - handle("c-e")(scroll_one_line_down) - handle("c-y")(scroll_one_line_up) - handle("pagedown")(scroll_page_down) - handle("pageup")(scroll_page_up) - - return ConditionalKeyBindings(key_bindings, vi_mode) +""" +Key bindings for extra page navigation: bindings for up/down scrolling through +long pages, like in Emacs or Vi. +""" +from prompt_toolkit.filters import buffer_has_focus, emacs_mode, vi_mode +from prompt_toolkit.key_binding.key_bindings import ( + ConditionalKeyBindings, + KeyBindings, + KeyBindingsBase, + merge_key_bindings, +) + +from .scroll import ( + scroll_backward, + scroll_forward, + scroll_half_page_down, + scroll_half_page_up, + scroll_one_line_down, + scroll_one_line_up, + scroll_page_down, + scroll_page_up, +) + +__all__ = [ + "load_page_navigation_bindings", + "load_emacs_page_navigation_bindings", + "load_vi_page_navigation_bindings", +] + + +def load_page_navigation_bindings() -> KeyBindingsBase: + """ + Load both the Vi and Emacs bindings for page navigation. + """ + # Only enable when a `Buffer` is focused, otherwise, we would catch keys + # when another widget is focused (like for instance `c-d` in a + # ptterm.Terminal). + return ConditionalKeyBindings( + merge_key_bindings( + [ + load_emacs_page_navigation_bindings(), + load_vi_page_navigation_bindings(), + ] + ), + buffer_has_focus, + ) + + +def load_emacs_page_navigation_bindings() -> KeyBindingsBase: + """ + Key bindings, for scrolling up and down through pages. + This are separate bindings, because GNU readline doesn't have them. + """ + key_bindings = KeyBindings() + handle = key_bindings.add + + handle("c-v")(scroll_page_down) + handle("pagedown")(scroll_page_down) + handle("escape", "v")(scroll_page_up) + handle("pageup")(scroll_page_up) + + return ConditionalKeyBindings(key_bindings, emacs_mode) + + +def load_vi_page_navigation_bindings() -> KeyBindingsBase: + """ + Key bindings, for scrolling up and down through pages. + This are separate bindings, because GNU readline doesn't have them. + """ + key_bindings = KeyBindings() + handle = key_bindings.add + + handle("c-f")(scroll_forward) + handle("c-b")(scroll_backward) + handle("c-d")(scroll_half_page_down) + handle("c-u")(scroll_half_page_up) + handle("c-e")(scroll_one_line_down) + handle("c-y")(scroll_one_line_up) + handle("pagedown")(scroll_page_down) + handle("pageup")(scroll_page_up) + + return ConditionalKeyBindings(key_bindings, vi_mode) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/scroll.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/scroll.py index 648e372304..4a43ff585a 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/scroll.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/scroll.py @@ -1,187 +1,187 @@ -""" -Key bindings, for scrolling up and down through pages. - -This are separate bindings, because GNU readline doesn't have them, but -they are very useful for navigating through long multiline buffers, like in -Vi, Emacs, etc... -""" -from prompt_toolkit.key_binding.key_processor import KeyPressEvent - -__all__ = [ - "scroll_forward", - "scroll_backward", - "scroll_half_page_up", - "scroll_half_page_down", - "scroll_one_line_up", - "scroll_one_line_down", -] - -E = KeyPressEvent - - -def scroll_forward(event: E, half: bool = False) -> None: - """ - Scroll window down. - """ - w = event.app.layout.current_window - b = event.app.current_buffer - - if w and w.render_info: - info = w.render_info - ui_content = info.ui_content - - # Height to scroll. - scroll_height = info.window_height - if half: - scroll_height //= 2 - - # Calculate how many lines is equivalent to that vertical space. - y = b.document.cursor_position_row + 1 - height = 0 - while y < ui_content.line_count: - line_height = info.get_height_for_line(y) - - if height + line_height < scroll_height: - height += line_height - y += 1 - else: - break - - b.cursor_position = b.document.translate_row_col_to_index(y, 0) - - -def scroll_backward(event: E, half: bool = False) -> None: - """ - Scroll window up. - """ - w = event.app.layout.current_window - b = event.app.current_buffer - - if w and w.render_info: - info = w.render_info - - # Height to scroll. - scroll_height = info.window_height - if half: - scroll_height //= 2 - - # Calculate how many lines is equivalent to that vertical space. - y = max(0, b.document.cursor_position_row - 1) - height = 0 - while y > 0: - line_height = info.get_height_for_line(y) - - if height + line_height < scroll_height: - height += line_height - y -= 1 - else: - break - - b.cursor_position = b.document.translate_row_col_to_index(y, 0) - - -def scroll_half_page_down(event: E) -> None: - """ - Same as ControlF, but only scroll half a page. - """ - scroll_forward(event, half=True) - - -def scroll_half_page_up(event: E) -> None: - """ - Same as ControlB, but only scroll half a page. - """ - scroll_backward(event, half=True) - - -def scroll_one_line_down(event: E) -> None: - """ - scroll_offset += 1 - """ - w = event.app.layout.current_window - b = event.app.current_buffer - - if w: - # When the cursor is at the top, move to the next line. (Otherwise, only scroll.) - if w.render_info: - info = w.render_info - - if w.vertical_scroll < info.content_height - info.window_height: - if info.cursor_position.y <= info.configured_scroll_offsets.top: - b.cursor_position += b.document.get_cursor_down_position() - - w.vertical_scroll += 1 - - -def scroll_one_line_up(event: E) -> None: - """ - scroll_offset -= 1 - """ - w = event.app.layout.current_window - b = event.app.current_buffer - - if w: - # When the cursor is at the bottom, move to the previous line. (Otherwise, only scroll.) - if w.render_info: - info = w.render_info - - if w.vertical_scroll > 0: - first_line_height = info.get_height_for_line(info.first_visible_line()) - - cursor_up = info.cursor_position.y - ( - info.window_height - - 1 - - first_line_height - - info.configured_scroll_offsets.bottom - ) - - # Move cursor up, as many steps as the height of the first line. - # TODO: not entirely correct yet, in case of line wrapping and many long lines. - for _ in range(max(0, cursor_up)): - b.cursor_position += b.document.get_cursor_up_position() - - # Scroll window - w.vertical_scroll -= 1 - - -def scroll_page_down(event: E) -> None: - """ - Scroll page down. (Prefer the cursor at the top of the page, after scrolling.) - """ - w = event.app.layout.current_window - b = event.app.current_buffer - - if w and w.render_info: - # Scroll down one page. - line_index = max(w.render_info.last_visible_line(), w.vertical_scroll + 1) - w.vertical_scroll = line_index - - b.cursor_position = b.document.translate_row_col_to_index(line_index, 0) - b.cursor_position += b.document.get_start_of_line_position( - after_whitespace=True - ) - - -def scroll_page_up(event: E) -> None: - """ - Scroll page up. (Prefer the cursor at the bottom of the page, after scrolling.) - """ - w = event.app.layout.current_window - b = event.app.current_buffer - - if w and w.render_info: - # Put cursor at the first visible line. (But make sure that the cursor - # moves at least one line up.) - line_index = max( - 0, - min(w.render_info.first_visible_line(), b.document.cursor_position_row - 1), - ) - - b.cursor_position = b.document.translate_row_col_to_index(line_index, 0) - b.cursor_position += b.document.get_start_of_line_position( - after_whitespace=True - ) - - # Set the scroll offset. We can safely set it to zero; the Window will - # make sure that it scrolls at least until the cursor becomes visible. - w.vertical_scroll = 0 +""" +Key bindings, for scrolling up and down through pages. + +This are separate bindings, because GNU readline doesn't have them, but +they are very useful for navigating through long multiline buffers, like in +Vi, Emacs, etc... +""" +from prompt_toolkit.key_binding.key_processor import KeyPressEvent + +__all__ = [ + "scroll_forward", + "scroll_backward", + "scroll_half_page_up", + "scroll_half_page_down", + "scroll_one_line_up", + "scroll_one_line_down", +] + +E = KeyPressEvent + + +def scroll_forward(event: E, half: bool = False) -> None: + """ + Scroll window down. + """ + w = event.app.layout.current_window + b = event.app.current_buffer + + if w and w.render_info: + info = w.render_info + ui_content = info.ui_content + + # Height to scroll. + scroll_height = info.window_height + if half: + scroll_height //= 2 + + # Calculate how many lines is equivalent to that vertical space. + y = b.document.cursor_position_row + 1 + height = 0 + while y < ui_content.line_count: + line_height = info.get_height_for_line(y) + + if height + line_height < scroll_height: + height += line_height + y += 1 + else: + break + + b.cursor_position = b.document.translate_row_col_to_index(y, 0) + + +def scroll_backward(event: E, half: bool = False) -> None: + """ + Scroll window up. + """ + w = event.app.layout.current_window + b = event.app.current_buffer + + if w and w.render_info: + info = w.render_info + + # Height to scroll. + scroll_height = info.window_height + if half: + scroll_height //= 2 + + # Calculate how many lines is equivalent to that vertical space. + y = max(0, b.document.cursor_position_row - 1) + height = 0 + while y > 0: + line_height = info.get_height_for_line(y) + + if height + line_height < scroll_height: + height += line_height + y -= 1 + else: + break + + b.cursor_position = b.document.translate_row_col_to_index(y, 0) + + +def scroll_half_page_down(event: E) -> None: + """ + Same as ControlF, but only scroll half a page. + """ + scroll_forward(event, half=True) + + +def scroll_half_page_up(event: E) -> None: + """ + Same as ControlB, but only scroll half a page. + """ + scroll_backward(event, half=True) + + +def scroll_one_line_down(event: E) -> None: + """ + scroll_offset += 1 + """ + w = event.app.layout.current_window + b = event.app.current_buffer + + if w: + # When the cursor is at the top, move to the next line. (Otherwise, only scroll.) + if w.render_info: + info = w.render_info + + if w.vertical_scroll < info.content_height - info.window_height: + if info.cursor_position.y <= info.configured_scroll_offsets.top: + b.cursor_position += b.document.get_cursor_down_position() + + w.vertical_scroll += 1 + + +def scroll_one_line_up(event: E) -> None: + """ + scroll_offset -= 1 + """ + w = event.app.layout.current_window + b = event.app.current_buffer + + if w: + # When the cursor is at the bottom, move to the previous line. (Otherwise, only scroll.) + if w.render_info: + info = w.render_info + + if w.vertical_scroll > 0: + first_line_height = info.get_height_for_line(info.first_visible_line()) + + cursor_up = info.cursor_position.y - ( + info.window_height + - 1 + - first_line_height + - info.configured_scroll_offsets.bottom + ) + + # Move cursor up, as many steps as the height of the first line. + # TODO: not entirely correct yet, in case of line wrapping and many long lines. + for _ in range(max(0, cursor_up)): + b.cursor_position += b.document.get_cursor_up_position() + + # Scroll window + w.vertical_scroll -= 1 + + +def scroll_page_down(event: E) -> None: + """ + Scroll page down. (Prefer the cursor at the top of the page, after scrolling.) + """ + w = event.app.layout.current_window + b = event.app.current_buffer + + if w and w.render_info: + # Scroll down one page. + line_index = max(w.render_info.last_visible_line(), w.vertical_scroll + 1) + w.vertical_scroll = line_index + + b.cursor_position = b.document.translate_row_col_to_index(line_index, 0) + b.cursor_position += b.document.get_start_of_line_position( + after_whitespace=True + ) + + +def scroll_page_up(event: E) -> None: + """ + Scroll page up. (Prefer the cursor at the bottom of the page, after scrolling.) + """ + w = event.app.layout.current_window + b = event.app.current_buffer + + if w and w.render_info: + # Put cursor at the first visible line. (But make sure that the cursor + # moves at least one line up.) + line_index = max( + 0, + min(w.render_info.first_visible_line(), b.document.cursor_position_row - 1), + ) + + b.cursor_position = b.document.translate_row_col_to_index(line_index, 0) + b.cursor_position += b.document.get_start_of_line_position( + after_whitespace=True + ) + + # Set the scroll offset. We can safely set it to zero; the Window will + # make sure that it scrolls at least until the cursor becomes visible. + w.vertical_scroll = 0 diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/search.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/search.py index 6c37ae1298..06a047e4cd 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/search.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/search.py @@ -1,93 +1,93 @@ -""" -Search related key bindings. -""" -from prompt_toolkit import search -from prompt_toolkit.application.current import get_app -from prompt_toolkit.filters import Condition, control_is_searchable, is_searching -from prompt_toolkit.key_binding.key_processor import KeyPressEvent - -from ..key_bindings import key_binding - -__all__ = [ - "abort_search", - "accept_search", - "start_reverse_incremental_search", - "start_forward_incremental_search", - "reverse_incremental_search", - "forward_incremental_search", - "accept_search_and_accept_input", -] - -E = KeyPressEvent - - -@key_binding(filter=is_searching) -def abort_search(event: E) -> None: - """ - Abort an incremental search and restore the original - line. - (Usually bound to ControlG/ControlC.) - """ - search.stop_search() - - -@key_binding(filter=is_searching) -def accept_search(event: E) -> None: - """ - When enter pressed in isearch, quit isearch mode. (Multiline - isearch would be too complicated.) - (Usually bound to Enter.) - """ - search.accept_search() - - -@key_binding(filter=control_is_searchable) -def start_reverse_incremental_search(event: E) -> None: - """ - Enter reverse incremental search. - (Usually ControlR.) - """ - search.start_search(direction=search.SearchDirection.BACKWARD) - - -@key_binding(filter=control_is_searchable) -def start_forward_incremental_search(event: E) -> None: - """ - Enter forward incremental search. - (Usually ControlS.) - """ - search.start_search(direction=search.SearchDirection.FORWARD) - - -@key_binding(filter=is_searching) -def reverse_incremental_search(event: E) -> None: - """ - Apply reverse incremental search, but keep search buffer focused. - """ - search.do_incremental_search(search.SearchDirection.BACKWARD, count=event.arg) - - -@key_binding(filter=is_searching) -def forward_incremental_search(event: E) -> None: - """ - Apply forward incremental search, but keep search buffer focused. - """ - search.do_incremental_search(search.SearchDirection.FORWARD, count=event.arg) - - -@Condition -def _previous_buffer_is_returnable() -> bool: - """ - True if the previously focused buffer has a return handler. - """ - prev_control = get_app().layout.search_target_buffer_control - return bool(prev_control and prev_control.buffer.is_returnable) - - -@key_binding(filter=is_searching & _previous_buffer_is_returnable) -def accept_search_and_accept_input(event: E) -> None: - """ - Accept the search operation first, then accept the input. - """ - search.accept_search() - event.current_buffer.validate_and_handle() +""" +Search related key bindings. +""" +from prompt_toolkit import search +from prompt_toolkit.application.current import get_app +from prompt_toolkit.filters import Condition, control_is_searchable, is_searching +from prompt_toolkit.key_binding.key_processor import KeyPressEvent + +from ..key_bindings import key_binding + +__all__ = [ + "abort_search", + "accept_search", + "start_reverse_incremental_search", + "start_forward_incremental_search", + "reverse_incremental_search", + "forward_incremental_search", + "accept_search_and_accept_input", +] + +E = KeyPressEvent + + +@key_binding(filter=is_searching) +def abort_search(event: E) -> None: + """ + Abort an incremental search and restore the original + line. + (Usually bound to ControlG/ControlC.) + """ + search.stop_search() + + +@key_binding(filter=is_searching) +def accept_search(event: E) -> None: + """ + When enter pressed in isearch, quit isearch mode. (Multiline + isearch would be too complicated.) + (Usually bound to Enter.) + """ + search.accept_search() + + +@key_binding(filter=control_is_searchable) +def start_reverse_incremental_search(event: E) -> None: + """ + Enter reverse incremental search. + (Usually ControlR.) + """ + search.start_search(direction=search.SearchDirection.BACKWARD) + + +@key_binding(filter=control_is_searchable) +def start_forward_incremental_search(event: E) -> None: + """ + Enter forward incremental search. + (Usually ControlS.) + """ + search.start_search(direction=search.SearchDirection.FORWARD) + + +@key_binding(filter=is_searching) +def reverse_incremental_search(event: E) -> None: + """ + Apply reverse incremental search, but keep search buffer focused. + """ + search.do_incremental_search(search.SearchDirection.BACKWARD, count=event.arg) + + +@key_binding(filter=is_searching) +def forward_incremental_search(event: E) -> None: + """ + Apply forward incremental search, but keep search buffer focused. + """ + search.do_incremental_search(search.SearchDirection.FORWARD, count=event.arg) + + +@Condition +def _previous_buffer_is_returnable() -> bool: + """ + True if the previously focused buffer has a return handler. + """ + prev_control = get_app().layout.search_target_buffer_control + return bool(prev_control and prev_control.buffer.is_returnable) + + +@key_binding(filter=is_searching & _previous_buffer_is_returnable) +def accept_search_and_accept_input(event: E) -> None: + """ + Accept the search operation first, then accept the input. + """ + search.accept_search() + event.current_buffer.validate_and_handle() diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/vi.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/vi.py index f202e2a7f9..efbb107de0 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/vi.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/vi.py @@ -1,2221 +1,2221 @@ -# pylint: disable=function-redefined -import codecs -import string -from enum import Enum -from itertools import accumulate +# pylint: disable=function-redefined +import codecs +import string +from enum import Enum +from itertools import accumulate from typing import Callable, Iterable, List, Optional, Tuple, TypeVar, Union - -from prompt_toolkit.application.current import get_app -from prompt_toolkit.buffer import Buffer, indent, reshape_text, unindent -from prompt_toolkit.clipboard import ClipboardData -from prompt_toolkit.document import Document -from prompt_toolkit.filters import ( - Always, - Condition, - Filter, - has_arg, - is_read_only, - is_searching, -) -from prompt_toolkit.filters.app import ( - in_paste_mode, - is_multiline, - vi_digraph_mode, - vi_insert_mode, - vi_insert_multiple_mode, - vi_mode, - vi_navigation_mode, - vi_recording_macro, - vi_replace_mode, - vi_replace_single_mode, - vi_search_direction_reversed, - vi_selection_mode, - vi_waiting_for_text_object_mode, -) -from prompt_toolkit.input.vt100_parser import Vt100Parser -from prompt_toolkit.key_binding.digraphs import DIGRAPHS -from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent -from prompt_toolkit.key_binding.vi_state import CharacterFind, InputMode -from prompt_toolkit.keys import Keys -from prompt_toolkit.search import SearchDirection -from prompt_toolkit.selection import PasteMode, SelectionState, SelectionType - -from ..key_bindings import ConditionalKeyBindings, KeyBindings, KeyBindingsBase -from .named_commands import get_by_name - -__all__ = [ - "load_vi_bindings", - "load_vi_search_bindings", -] - -E = KeyPressEvent - -ascii_lowercase = string.ascii_lowercase - -vi_register_names = ascii_lowercase + "0123456789" - - -class TextObjectType(Enum): - EXCLUSIVE = "EXCLUSIVE" - INCLUSIVE = "INCLUSIVE" - LINEWISE = "LINEWISE" - BLOCK = "BLOCK" - - -class TextObject: - """ - Return struct for functions wrapped in ``text_object``. - Both `start` and `end` are relative to the current cursor position. - """ - - def __init__( - self, start: int, end: int = 0, type: TextObjectType = TextObjectType.EXCLUSIVE - ): - - self.start = start - self.end = end - self.type = type - - @property - def selection_type(self) -> SelectionType: - if self.type == TextObjectType.LINEWISE: - return SelectionType.LINES - if self.type == TextObjectType.BLOCK: - return SelectionType.BLOCK - else: - return SelectionType.CHARACTERS - - def sorted(self) -> Tuple[int, int]: - """ - Return a (start, end) tuple where start <= end. - """ - if self.start < self.end: - return self.start, self.end - else: - return self.end, self.start - - def operator_range(self, document: Document) -> Tuple[int, int]: - """ - Return a (start, end) tuple with start <= end that indicates the range - operators should operate on. - `buffer` is used to get start and end of line positions. - - This should return something that can be used in a slice, so the `end` - position is *not* included. - """ - start, end = self.sorted() - doc = document - - if ( - self.type == TextObjectType.EXCLUSIVE - and doc.translate_index_to_position(end + doc.cursor_position)[1] == 0 - ): - # If the motion is exclusive and the end of motion is on the first - # column, the end position becomes end of previous line. - end -= 1 - if self.type == TextObjectType.INCLUSIVE: - end += 1 - if self.type == TextObjectType.LINEWISE: - # Select whole lines - row, col = doc.translate_index_to_position(start + doc.cursor_position) - start = doc.translate_row_col_to_index(row, 0) - doc.cursor_position - row, col = doc.translate_index_to_position(end + doc.cursor_position) - end = ( - doc.translate_row_col_to_index(row, len(doc.lines[row])) - - doc.cursor_position - ) - return start, end - - def get_line_numbers(self, buffer: Buffer) -> Tuple[int, int]: - """ - Return a (start_line, end_line) pair. - """ - # Get absolute cursor positions from the text object. - from_, to = self.operator_range(buffer.document) - from_ += buffer.cursor_position - to += buffer.cursor_position - - # Take the start of the lines. - from_, _ = buffer.document.translate_index_to_position(from_) - to, _ = buffer.document.translate_index_to_position(to) - - return from_, to - - def cut(self, buffer: Buffer) -> Tuple[Document, ClipboardData]: - """ - Turn text object into `ClipboardData` instance. - """ - from_, to = self.operator_range(buffer.document) - - from_ += buffer.cursor_position - to += buffer.cursor_position - - # For Vi mode, the SelectionState does include the upper position, - # while `self.operator_range` does not. So, go one to the left, unless - # we're in the line mode, then we don't want to risk going to the - # previous line, and missing one line in the selection. - if self.type != TextObjectType.LINEWISE: - to -= 1 - - document = Document( - buffer.text, - to, - SelectionState(original_cursor_position=from_, type=self.selection_type), - ) - - new_document, clipboard_data = document.cut_selection() - return new_document, clipboard_data - - -# Typevar for any text object function: -TextObjectFunction = Callable[[E], TextObject] -_TOF = TypeVar("_TOF", bound=TextObjectFunction) - - -def create_text_object_decorator( - key_bindings: KeyBindings, -) -> Callable[..., Callable[[_TOF], _TOF]]: - """ - Create a decorator that can be used to register Vi text object implementations. - """ - - def text_object_decorator( - *keys: Union[Keys, str], - filter: Filter = Always(), - no_move_handler: bool = False, - no_selection_handler: bool = False, - eager: bool = False, - ) -> Callable[[_TOF], _TOF]: - """ - Register a text object function. - - Usage:: - - @text_object('w', filter=..., no_move_handler=False) - def handler(event): - # Return a text object for this key. - return TextObject(...) - - :param no_move_handler: Disable the move handler in navigation mode. - (It's still active in selection mode.) - """ - - def decorator(text_object_func: _TOF) -> _TOF: - @key_bindings.add( - *keys, filter=vi_waiting_for_text_object_mode & filter, eager=eager - ) - def _apply_operator_to_text_object(event: E) -> None: - # Arguments are multiplied. - vi_state = event.app.vi_state - event._arg = str((vi_state.operator_arg or 1) * (event.arg or 1)) - - # Call the text object handler. - text_obj = text_object_func(event) - - # Get the operator function. - # (Should never be None here, given the - # `vi_waiting_for_text_object_mode` filter state.) - operator_func = vi_state.operator_func - - if text_obj is not None and operator_func is not None: - # Call the operator function with the text object. - operator_func(event, text_obj) - - # Clear operator. - event.app.vi_state.operator_func = None - event.app.vi_state.operator_arg = None - - # Register a move operation. (Doesn't need an operator.) - if not no_move_handler: - - @key_bindings.add( - *keys, - filter=~vi_waiting_for_text_object_mode - & filter - & vi_navigation_mode, - eager=eager, - ) - def _move_in_navigation_mode(event: E) -> None: - """ - Move handler for navigation mode. - """ - text_object = text_object_func(event) - event.current_buffer.cursor_position += text_object.start - - # Register a move selection operation. - if not no_selection_handler: - - @key_bindings.add( - *keys, - filter=~vi_waiting_for_text_object_mode - & filter - & vi_selection_mode, - eager=eager, - ) - def _move_in_selection_mode(event: E) -> None: - """ - Move handler for selection mode. - """ - text_object = text_object_func(event) - buff = event.current_buffer - selection_state = buff.selection_state - - if selection_state is None: - return # Should not happen, because of the `vi_selection_mode` filter. - - # When the text object has both a start and end position, like 'i(' or 'iw', - # Turn this into a selection, otherwise the cursor. - if text_object.end: - # Take selection positions from text object. - start, end = text_object.operator_range(buff.document) - start += buff.cursor_position - end += buff.cursor_position - - selection_state.original_cursor_position = start - buff.cursor_position = end - - # Take selection type from text object. - if text_object.type == TextObjectType.LINEWISE: - selection_state.type = SelectionType.LINES - else: - selection_state.type = SelectionType.CHARACTERS - else: - event.current_buffer.cursor_position += text_object.start - - # Make it possible to chain @text_object decorators. - return text_object_func - - return decorator - - return text_object_decorator - - -# Typevar for any operator function: -OperatorFunction = Callable[[E, TextObject], None] -_OF = TypeVar("_OF", bound=OperatorFunction) - - -def create_operator_decorator( - key_bindings: KeyBindings, -) -> Callable[..., Callable[[_OF], _OF]]: - """ - Create a decorator that can be used for registering Vi operators. - """ - - def operator_decorator( - *keys: Union[Keys, str], filter: Filter = Always(), eager: bool = False - ) -> Callable[[_OF], _OF]: - """ - Register a Vi operator. - - Usage:: - - @operator('d', filter=...) - def handler(event, text_object): - # Do something with the text object here. - """ - - def decorator(operator_func: _OF) -> _OF: - @key_bindings.add( - *keys, - filter=~vi_waiting_for_text_object_mode & filter & vi_navigation_mode, - eager=eager, - ) - def _operator_in_navigation(event: E) -> None: - """ - Handle operator in navigation mode. - """ - # When this key binding is matched, only set the operator - # function in the ViState. We should execute it after a text - # object has been received. - event.app.vi_state.operator_func = operator_func - event.app.vi_state.operator_arg = event.arg - - @key_bindings.add( - *keys, - filter=~vi_waiting_for_text_object_mode & filter & vi_selection_mode, - eager=eager, - ) - def _operator_in_selection(event: E) -> None: - """ - Handle operator in selection mode. - """ - buff = event.current_buffer - selection_state = buff.selection_state - - if selection_state is not None: - # Create text object from selection. - if selection_state.type == SelectionType.LINES: - text_obj_type = TextObjectType.LINEWISE - elif selection_state.type == SelectionType.BLOCK: - text_obj_type = TextObjectType.BLOCK - else: - text_obj_type = TextObjectType.INCLUSIVE - - text_object = TextObject( - selection_state.original_cursor_position - buff.cursor_position, - type=text_obj_type, - ) - - # Execute operator. - operator_func(event, text_object) - - # Quit selection mode. - buff.selection_state = None - - return operator_func - - return decorator - - return operator_decorator - - -def load_vi_bindings() -> KeyBindingsBase: - """ - Vi extensions. - - # Overview of Readline Vi commands: - # http://www.catonmat.net/download/bash-vi-editing-mode-cheat-sheet.pdf - """ - # Note: Some key bindings have the "~IsReadOnly()" filter added. This - # prevents the handler to be executed when the focus is on a - # read-only buffer. - # This is however only required for those that change the ViState to - # INSERT mode. The `Buffer` class itself throws the - # `EditReadOnlyBuffer` exception for any text operations which is - # handled correctly. There is no need to add "~IsReadOnly" to all key - # bindings that do text manipulation. - - key_bindings = KeyBindings() - handle = key_bindings.add - - # (Note: Always take the navigation bindings in read-only mode, even when - # ViState says different.) - - TransformFunction = Tuple[Tuple[str, ...], Filter, Callable[[str], str]] - - vi_transform_functions: List[TransformFunction] = [ - # Rot 13 transformation - ( - ("g", "?"), - Always(), - lambda string: codecs.encode(string, "rot_13"), - ), - # To lowercase - (("g", "u"), Always(), lambda string: string.lower()), - # To uppercase. - (("g", "U"), Always(), lambda string: string.upper()), - # Swap case. - (("g", "~"), Always(), lambda string: string.swapcase()), - ( - ("~",), - Condition(lambda: get_app().vi_state.tilde_operator), - lambda string: string.swapcase(), - ), - ] - - # Insert a character literally (quoted insert). - handle("c-v", filter=vi_insert_mode)(get_by_name("quoted-insert")) - - @handle("escape") - def _back_to_navigation(event: E) -> None: - """ - Escape goes to vi navigation mode. - """ - buffer = event.current_buffer - vi_state = event.app.vi_state - - if vi_state.input_mode in (InputMode.INSERT, InputMode.REPLACE): - buffer.cursor_position += buffer.document.get_cursor_left_position() - - vi_state.input_mode = InputMode.NAVIGATION - - if bool(buffer.selection_state): - buffer.exit_selection() - - @handle("k", filter=vi_selection_mode) - def _up_in_selection(event: E) -> None: - """ - Arrow up in selection mode. - """ - event.current_buffer.cursor_up(count=event.arg) - - @handle("j", filter=vi_selection_mode) - def _down_in_selection(event: E) -> None: - """ - Arrow down in selection mode. - """ - event.current_buffer.cursor_down(count=event.arg) - - @handle("up", filter=vi_navigation_mode) - @handle("c-p", filter=vi_navigation_mode) - def _up_in_navigation(event: E) -> None: - """ - Arrow up and ControlP in navigation mode go up. - """ - event.current_buffer.auto_up(count=event.arg) - - @handle("k", filter=vi_navigation_mode) - def _go_up(event: E) -> None: - """ - Go up, but if we enter a new history entry, move to the start of the - line. - """ - event.current_buffer.auto_up( - count=event.arg, go_to_start_of_line_if_history_changes=True - ) - - @handle("down", filter=vi_navigation_mode) - @handle("c-n", filter=vi_navigation_mode) - def _go_down(event: E) -> None: - """ - Arrow down and Control-N in navigation mode. - """ - event.current_buffer.auto_down(count=event.arg) - - @handle("j", filter=vi_navigation_mode) - def _go_down2(event: E) -> None: - """ - Go down, but if we enter a new history entry, go to the start of the line. - """ - event.current_buffer.auto_down( - count=event.arg, go_to_start_of_line_if_history_changes=True - ) - - @handle("backspace", filter=vi_navigation_mode) - def _go_left(event: E) -> None: - """ - In navigation-mode, move cursor. - """ - event.current_buffer.cursor_position += ( - event.current_buffer.document.get_cursor_left_position(count=event.arg) - ) - - @handle("c-n", filter=vi_insert_mode) - def _complete_next(event: E) -> None: - b = event.current_buffer - - if b.complete_state: - b.complete_next() - else: - b.start_completion(select_first=True) - - @handle("c-p", filter=vi_insert_mode) - def _complete_prev(event: E) -> None: - """ - Control-P: To previous completion. - """ - b = event.current_buffer - - if b.complete_state: - b.complete_previous() - else: - b.start_completion(select_last=True) - - @handle("c-g", filter=vi_insert_mode) - @handle("c-y", filter=vi_insert_mode) - def _accept_completion(event: E) -> None: - """ - Accept current completion. - """ - event.current_buffer.complete_state = None - - @handle("c-e", filter=vi_insert_mode) - def _cancel_completion(event: E) -> None: - """ - Cancel completion. Go back to originally typed text. - """ - event.current_buffer.cancel_completion() - - @Condition - def is_returnable() -> bool: - return get_app().current_buffer.is_returnable - - # In navigation mode, pressing enter will always return the input. - handle("enter", filter=vi_navigation_mode & is_returnable)( - get_by_name("accept-line") - ) - - # In insert mode, also accept input when enter is pressed, and the buffer - # has been marked as single line. - handle("enter", filter=is_returnable & ~is_multiline)(get_by_name("accept-line")) - - @handle("enter", filter=~is_returnable & vi_navigation_mode) - def _start_of_next_line(event: E) -> None: - """ - Go to the beginning of next line. - """ - b = event.current_buffer - b.cursor_down(count=event.arg) - b.cursor_position += b.document.get_start_of_line_position( - after_whitespace=True - ) - - # ** In navigation mode ** - - # List of navigation commands: http://hea-www.harvard.edu/~fine/Tech/vi.html - - @handle("insert", filter=vi_navigation_mode) - def _insert_mode(event: E) -> None: - """ - Pressing the Insert key. - """ - event.app.vi_state.input_mode = InputMode.INSERT - - @handle("insert", filter=vi_insert_mode) - def _navigation_mode(event: E) -> None: - """ - Pressing the Insert key. - """ - event.app.vi_state.input_mode = InputMode.NAVIGATION - - @handle("a", filter=vi_navigation_mode & ~is_read_only) - # ~IsReadOnly, because we want to stay in navigation mode for - # read-only buffers. - def _a(event: E) -> None: - event.current_buffer.cursor_position += ( - event.current_buffer.document.get_cursor_right_position() - ) - event.app.vi_state.input_mode = InputMode.INSERT - - @handle("A", filter=vi_navigation_mode & ~is_read_only) - def _A(event: E) -> None: - event.current_buffer.cursor_position += ( - event.current_buffer.document.get_end_of_line_position() - ) - event.app.vi_state.input_mode = InputMode.INSERT - - @handle("C", filter=vi_navigation_mode & ~is_read_only) - def _change_until_end_of_line(event: E) -> None: - """ - Change to end of line. - Same as 'c$' (which is implemented elsewhere.) - """ - buffer = event.current_buffer - - deleted = buffer.delete(count=buffer.document.get_end_of_line_position()) - event.app.clipboard.set_text(deleted) - event.app.vi_state.input_mode = InputMode.INSERT - - @handle("c", "c", filter=vi_navigation_mode & ~is_read_only) - @handle("S", filter=vi_navigation_mode & ~is_read_only) - def _change_current_line(event: E) -> None: # TODO: implement 'arg' - """ - Change current line - """ - buffer = event.current_buffer - - # We copy the whole line. - data = ClipboardData(buffer.document.current_line, SelectionType.LINES) - event.app.clipboard.set_data(data) - - # But we delete after the whitespace - buffer.cursor_position += buffer.document.get_start_of_line_position( - after_whitespace=True - ) - buffer.delete(count=buffer.document.get_end_of_line_position()) - event.app.vi_state.input_mode = InputMode.INSERT - - @handle("D", filter=vi_navigation_mode) - def _delete_until_end_of_line(event: E) -> None: - """ - Delete from cursor position until the end of the line. - """ - buffer = event.current_buffer - deleted = buffer.delete(count=buffer.document.get_end_of_line_position()) - event.app.clipboard.set_text(deleted) - - @handle("d", "d", filter=vi_navigation_mode) - def _delete_line(event: E) -> None: - """ - Delete line. (Or the following 'n' lines.) - """ - buffer = event.current_buffer - - # Split string in before/deleted/after text. - lines = buffer.document.lines - - before = "\n".join(lines[: buffer.document.cursor_position_row]) - deleted = "\n".join( - lines[ - buffer.document.cursor_position_row : buffer.document.cursor_position_row - + event.arg - ] - ) - after = "\n".join(lines[buffer.document.cursor_position_row + event.arg :]) - - # Set new text. - if before and after: - before = before + "\n" - - # Set text and cursor position. - buffer.document = Document( - text=before + after, - # Cursor At the start of the first 'after' line, after the leading whitespace. - cursor_position=len(before) + len(after) - len(after.lstrip(" ")), - ) - - # Set clipboard data - event.app.clipboard.set_data(ClipboardData(deleted, SelectionType.LINES)) - - @handle("x", filter=vi_selection_mode) - def _cut(event: E) -> None: - """ - Cut selection. - ('x' is not an operator.) - """ - clipboard_data = event.current_buffer.cut_selection() - event.app.clipboard.set_data(clipboard_data) - - @handle("i", filter=vi_navigation_mode & ~is_read_only) - def _i(event: E) -> None: - event.app.vi_state.input_mode = InputMode.INSERT - - @handle("I", filter=vi_navigation_mode & ~is_read_only) - def _I(event: E) -> None: - event.app.vi_state.input_mode = InputMode.INSERT - event.current_buffer.cursor_position += ( - event.current_buffer.document.get_start_of_line_position( - after_whitespace=True - ) - ) - - @Condition - def in_block_selection() -> bool: - buff = get_app().current_buffer - return bool( - buff.selection_state and buff.selection_state.type == SelectionType.BLOCK - ) - - @handle("I", filter=in_block_selection & ~is_read_only) - def insert_in_block_selection(event: E, after: bool = False) -> None: - """ - Insert in block selection mode. - """ - buff = event.current_buffer - - # Store all cursor positions. - positions = [] - - if after: - - def get_pos(from_to: Tuple[int, int]) -> int: - return from_to[1] - - else: - - def get_pos(from_to: Tuple[int, int]) -> int: - return from_to[0] - - for i, from_to in enumerate(buff.document.selection_ranges()): - positions.append(get_pos(from_to)) - if i == 0: - buff.cursor_position = get_pos(from_to) - - buff.multiple_cursor_positions = positions - - # Go to 'INSERT_MULTIPLE' mode. - event.app.vi_state.input_mode = InputMode.INSERT_MULTIPLE - buff.exit_selection() - - @handle("A", filter=in_block_selection & ~is_read_only) - def _append_after_block(event: E) -> None: - insert_in_block_selection(event, after=True) - - @handle("J", filter=vi_navigation_mode & ~is_read_only) - def _join(event: E) -> None: - """ - Join lines. - """ - for i in range(event.arg): - event.current_buffer.join_next_line() - - @handle("g", "J", filter=vi_navigation_mode & ~is_read_only) - def _join_nospace(event: E) -> None: - """ - Join lines without space. - """ - for i in range(event.arg): - event.current_buffer.join_next_line(separator="") - - @handle("J", filter=vi_selection_mode & ~is_read_only) - def _join_selection(event: E) -> None: - """ - Join selected lines. - """ - event.current_buffer.join_selected_lines() - - @handle("g", "J", filter=vi_selection_mode & ~is_read_only) - def _join_selection_nospace(event: E) -> None: - """ - Join selected lines without space. - """ - event.current_buffer.join_selected_lines(separator="") - - @handle("p", filter=vi_navigation_mode) - def _paste(event: E) -> None: - """ - Paste after - """ - event.current_buffer.paste_clipboard_data( - event.app.clipboard.get_data(), - count=event.arg, - paste_mode=PasteMode.VI_AFTER, - ) - - @handle("P", filter=vi_navigation_mode) - def _paste_before(event: E) -> None: - """ - Paste before - """ - event.current_buffer.paste_clipboard_data( - event.app.clipboard.get_data(), - count=event.arg, - paste_mode=PasteMode.VI_BEFORE, - ) - - @handle('"', Keys.Any, "p", filter=vi_navigation_mode) - def _paste_register(event: E) -> None: - """ - Paste from named register. - """ - c = event.key_sequence[1].data - if c in vi_register_names: - data = event.app.vi_state.named_registers.get(c) - if data: - event.current_buffer.paste_clipboard_data( - data, count=event.arg, paste_mode=PasteMode.VI_AFTER - ) - - @handle('"', Keys.Any, "P", filter=vi_navigation_mode) - def _paste_register_before(event: E) -> None: - """ - Paste (before) from named register. - """ - c = event.key_sequence[1].data - if c in vi_register_names: - data = event.app.vi_state.named_registers.get(c) - if data: - event.current_buffer.paste_clipboard_data( - data, count=event.arg, paste_mode=PasteMode.VI_BEFORE - ) - - @handle("r", filter=vi_navigation_mode) - def _replace(event: E) -> None: - """ - Go to 'replace-single'-mode. - """ - event.app.vi_state.input_mode = InputMode.REPLACE_SINGLE - - @handle("R", filter=vi_navigation_mode) - def _replace_mode(event: E) -> None: - """ - Go to 'replace'-mode. - """ - event.app.vi_state.input_mode = InputMode.REPLACE - - @handle("s", filter=vi_navigation_mode & ~is_read_only) - def _substitute(event: E) -> None: - """ - Substitute with new text - (Delete character(s) and go to insert mode.) - """ - text = event.current_buffer.delete(count=event.arg) - event.app.clipboard.set_text(text) - event.app.vi_state.input_mode = InputMode.INSERT - - @handle("u", filter=vi_navigation_mode, save_before=(lambda e: False)) - def _undo(event: E) -> None: - for i in range(event.arg): - event.current_buffer.undo() - - @handle("V", filter=vi_navigation_mode) - def _visual_line(event: E) -> None: - """ - Start lines selection. - """ - event.current_buffer.start_selection(selection_type=SelectionType.LINES) - - @handle("c-v", filter=vi_navigation_mode) - def _visual_block(event: E) -> None: - """ - Enter block selection mode. - """ - event.current_buffer.start_selection(selection_type=SelectionType.BLOCK) - - @handle("V", filter=vi_selection_mode) - def _visual_line2(event: E) -> None: - """ - Exit line selection mode, or go from non line selection mode to line - selection mode. - """ - selection_state = event.current_buffer.selection_state - - if selection_state is not None: - if selection_state.type != SelectionType.LINES: - selection_state.type = SelectionType.LINES - else: - event.current_buffer.exit_selection() - - @handle("v", filter=vi_navigation_mode) - def _visual(event: E) -> None: - """ - Enter character selection mode. - """ - event.current_buffer.start_selection(selection_type=SelectionType.CHARACTERS) - - @handle("v", filter=vi_selection_mode) - def _visual2(event: E) -> None: - """ - Exit character selection mode, or go from non-character-selection mode - to character selection mode. - """ - selection_state = event.current_buffer.selection_state - - if selection_state is not None: - if selection_state.type != SelectionType.CHARACTERS: - selection_state.type = SelectionType.CHARACTERS - else: - event.current_buffer.exit_selection() - - @handle("c-v", filter=vi_selection_mode) - def _visual_block2(event: E) -> None: - """ - Exit block selection mode, or go from non block selection mode to block - selection mode. - """ - selection_state = event.current_buffer.selection_state - - if selection_state is not None: - if selection_state.type != SelectionType.BLOCK: - selection_state.type = SelectionType.BLOCK - else: - event.current_buffer.exit_selection() - - @handle("a", "w", filter=vi_selection_mode) - @handle("a", "W", filter=vi_selection_mode) - def _visual_auto_word(event: E) -> None: - """ - Switch from visual linewise mode to visual characterwise mode. - """ - buffer = event.current_buffer - - if ( - buffer.selection_state - and buffer.selection_state.type == SelectionType.LINES - ): - buffer.selection_state.type = SelectionType.CHARACTERS - - @handle("x", filter=vi_navigation_mode) - def _delete(event: E) -> None: - """ - Delete character. - """ - buff = event.current_buffer - count = min(event.arg, len(buff.document.current_line_after_cursor)) - if count: - text = event.current_buffer.delete(count=count) - event.app.clipboard.set_text(text) - - @handle("X", filter=vi_navigation_mode) - def _delete_before_cursor(event: E) -> None: - buff = event.current_buffer - count = min(event.arg, len(buff.document.current_line_before_cursor)) - if count: - text = event.current_buffer.delete_before_cursor(count=count) - event.app.clipboard.set_text(text) - - @handle("y", "y", filter=vi_navigation_mode) - @handle("Y", filter=vi_navigation_mode) - def _yank_line(event: E) -> None: - """ - Yank the whole line. - """ - text = "\n".join(event.current_buffer.document.lines_from_current[: event.arg]) - event.app.clipboard.set_data(ClipboardData(text, SelectionType.LINES)) - - @handle("+", filter=vi_navigation_mode) - def _next_line(event: E) -> None: - """ - Move to first non whitespace of next line - """ - buffer = event.current_buffer - buffer.cursor_position += buffer.document.get_cursor_down_position( - count=event.arg - ) - buffer.cursor_position += buffer.document.get_start_of_line_position( - after_whitespace=True - ) - - @handle("-", filter=vi_navigation_mode) - def _prev_line(event: E) -> None: - """ - Move to first non whitespace of previous line - """ - buffer = event.current_buffer - buffer.cursor_position += buffer.document.get_cursor_up_position( - count=event.arg - ) - buffer.cursor_position += buffer.document.get_start_of_line_position( - after_whitespace=True - ) - - @handle(">", ">", filter=vi_navigation_mode) - def _indent(event: E) -> None: - """ - Indent lines. - """ - buffer = event.current_buffer - current_row = buffer.document.cursor_position_row - indent(buffer, current_row, current_row + event.arg) - - @handle("<", "<", filter=vi_navigation_mode) - def _unindent(event: E) -> None: - """ - Unindent lines. - """ - current_row = event.current_buffer.document.cursor_position_row - unindent(event.current_buffer, current_row, current_row + event.arg) - - @handle("O", filter=vi_navigation_mode & ~is_read_only) - def _open_above(event: E) -> None: - """ - Open line above and enter insertion mode - """ - event.current_buffer.insert_line_above(copy_margin=not in_paste_mode()) - event.app.vi_state.input_mode = InputMode.INSERT - - @handle("o", filter=vi_navigation_mode & ~is_read_only) - def _open_below(event: E) -> None: - """ - Open line below and enter insertion mode - """ - event.current_buffer.insert_line_below(copy_margin=not in_paste_mode()) - event.app.vi_state.input_mode = InputMode.INSERT - - @handle("~", filter=vi_navigation_mode) - def _reverse_case(event: E) -> None: - """ - Reverse case of current character and move cursor forward. - """ - buffer = event.current_buffer - c = buffer.document.current_char - - if c is not None and c != "\n": - buffer.insert_text(c.swapcase(), overwrite=True) - - @handle("g", "u", "u", filter=vi_navigation_mode & ~is_read_only) - def _lowercase_line(event: E) -> None: - """ - Lowercase current line. - """ - buff = event.current_buffer - buff.transform_current_line(lambda s: s.lower()) - - @handle("g", "U", "U", filter=vi_navigation_mode & ~is_read_only) - def _uppercase_line(event: E) -> None: - """ - Uppercase current line. - """ - buff = event.current_buffer - buff.transform_current_line(lambda s: s.upper()) - - @handle("g", "~", "~", filter=vi_navigation_mode & ~is_read_only) - def _swapcase_line(event: E) -> None: - """ - Swap case of the current line. - """ - buff = event.current_buffer - buff.transform_current_line(lambda s: s.swapcase()) - - @handle("#", filter=vi_navigation_mode) - def _prev_occurence(event: E) -> None: - """ - Go to previous occurrence of this word. - """ - b = event.current_buffer - search_state = event.app.current_search_state - - search_state.text = b.document.get_word_under_cursor() - search_state.direction = SearchDirection.BACKWARD - - b.apply_search(search_state, count=event.arg, include_current_position=False) - - @handle("*", filter=vi_navigation_mode) - def _next_occurance(event: E) -> None: - """ - Go to next occurrence of this word. - """ - b = event.current_buffer - search_state = event.app.current_search_state - - search_state.text = b.document.get_word_under_cursor() - search_state.direction = SearchDirection.FORWARD - - b.apply_search(search_state, count=event.arg, include_current_position=False) - - @handle("(", filter=vi_navigation_mode) - def _begin_of_sentence(event: E) -> None: - # TODO: go to begin of sentence. - # XXX: should become text_object. - pass - - @handle(")", filter=vi_navigation_mode) - def _end_of_sentence(event: E) -> None: - # TODO: go to end of sentence. - # XXX: should become text_object. - pass - - operator = create_operator_decorator(key_bindings) - text_object = create_text_object_decorator(key_bindings) - - @handle(Keys.Any, filter=vi_waiting_for_text_object_mode) - def _unknown_text_object(event: E) -> None: - """ - Unknown key binding while waiting for a text object. - """ - event.app.output.bell() - - # - # *** Operators *** - # - - def create_delete_and_change_operators( - delete_only: bool, with_register: bool = False - ) -> None: - """ - Delete and change operators. - - :param delete_only: Create an operator that deletes, but doesn't go to insert mode. - :param with_register: Copy the deleted text to this named register instead of the clipboard. - """ - handler_keys: Iterable[str] - if with_register: - handler_keys = ('"', Keys.Any, "cd"[delete_only]) - else: - handler_keys = "cd"[delete_only] - - @operator(*handler_keys, filter=~is_read_only) - def delete_or_change_operator(event: E, text_object: TextObject) -> None: - clipboard_data = None - buff = event.current_buffer - - if text_object: - new_document, clipboard_data = text_object.cut(buff) - buff.document = new_document - - # Set deleted/changed text to clipboard or named register. - if clipboard_data and clipboard_data.text: - if with_register: - reg_name = event.key_sequence[1].data - if reg_name in vi_register_names: - event.app.vi_state.named_registers[reg_name] = clipboard_data - else: - event.app.clipboard.set_data(clipboard_data) - - # Only go back to insert mode in case of 'change'. - if not delete_only: - event.app.vi_state.input_mode = InputMode.INSERT - - create_delete_and_change_operators(False, False) - create_delete_and_change_operators(False, True) - create_delete_and_change_operators(True, False) - create_delete_and_change_operators(True, True) - - def create_transform_handler( - filter: Filter, transform_func: Callable[[str], str], *a: str - ) -> None: - @operator(*a, filter=filter & ~is_read_only) - def _(event: E, text_object: TextObject) -> None: - """ - Apply transformation (uppercase, lowercase, rot13, swap case). - """ - buff = event.current_buffer - start, end = text_object.operator_range(buff.document) - - if start < end: - # Transform. - buff.transform_region( - buff.cursor_position + start, - buff.cursor_position + end, - transform_func, - ) - - # Move cursor - buff.cursor_position += text_object.end or text_object.start - - for k, f, func in vi_transform_functions: - create_transform_handler(f, func, *k) - - @operator("y") - def _yank(event: E, text_object: TextObject) -> None: - """ - Yank operator. (Copy text.) - """ - _, clipboard_data = text_object.cut(event.current_buffer) - if clipboard_data.text: - event.app.clipboard.set_data(clipboard_data) - - @operator('"', Keys.Any, "y") - def _yank_to_register(event: E, text_object: TextObject) -> None: - """ - Yank selection to named register. - """ - c = event.key_sequence[1].data - if c in vi_register_names: - _, clipboard_data = text_object.cut(event.current_buffer) - event.app.vi_state.named_registers[c] = clipboard_data - - @operator(">") - def _indent_text_object(event: E, text_object: TextObject) -> None: - """ - Indent. - """ - buff = event.current_buffer - from_, to = text_object.get_line_numbers(buff) - indent(buff, from_, to + 1, count=event.arg) - - @operator("<") - def _unindent_text_object(event: E, text_object: TextObject) -> None: - """ - Unindent. - """ - buff = event.current_buffer - from_, to = text_object.get_line_numbers(buff) - unindent(buff, from_, to + 1, count=event.arg) - - @operator("g", "q") - def _reshape(event: E, text_object: TextObject) -> None: - """ - Reshape text. - """ - buff = event.current_buffer - from_, to = text_object.get_line_numbers(buff) - reshape_text(buff, from_, to) - - # - # *** Text objects *** - # - - @text_object("b") - def _b(event: E) -> TextObject: - """ - Move one word or token left. - """ - return TextObject( - event.current_buffer.document.find_start_of_previous_word(count=event.arg) - or 0 - ) - - @text_object("B") - def _B(event: E) -> TextObject: - """ - Move one non-blank word left - """ - return TextObject( - event.current_buffer.document.find_start_of_previous_word( - count=event.arg, WORD=True - ) - or 0 - ) - - @text_object("$") - def _dollar(event: E) -> TextObject: - """ - 'c$', 'd$' and '$': Delete/change/move until end of line. - """ - return TextObject(event.current_buffer.document.get_end_of_line_position()) - - @text_object("w") - def _word_forward(event: E) -> TextObject: - """ - 'word' forward. 'cw', 'dw', 'w': Delete/change/move one word. - """ - return TextObject( - event.current_buffer.document.find_next_word_beginning(count=event.arg) - or event.current_buffer.document.get_end_of_document_position() - ) - - @text_object("W") - def _WORD_forward(event: E) -> TextObject: - """ - 'WORD' forward. 'cW', 'dW', 'W': Delete/change/move one WORD. - """ - return TextObject( - event.current_buffer.document.find_next_word_beginning( - count=event.arg, WORD=True - ) - or event.current_buffer.document.get_end_of_document_position() - ) - - @text_object("e") - def _end_of_word(event: E) -> TextObject: - """ - End of 'word': 'ce', 'de', 'e' - """ - end = event.current_buffer.document.find_next_word_ending(count=event.arg) - return TextObject(end - 1 if end else 0, type=TextObjectType.INCLUSIVE) - - @text_object("E") - def _end_of_WORD(event: E) -> TextObject: - """ - End of 'WORD': 'cE', 'dE', 'E' - """ - end = event.current_buffer.document.find_next_word_ending( - count=event.arg, WORD=True - ) - return TextObject(end - 1 if end else 0, type=TextObjectType.INCLUSIVE) - - @text_object("i", "w", no_move_handler=True) - def _inner_word(event: E) -> TextObject: - """ - Inner 'word': ciw and diw - """ - start, end = event.current_buffer.document.find_boundaries_of_current_word() - return TextObject(start, end) - - @text_object("a", "w", no_move_handler=True) - def _a_word(event: E) -> TextObject: - """ - A 'word': caw and daw - """ - start, end = event.current_buffer.document.find_boundaries_of_current_word( - include_trailing_whitespace=True - ) - return TextObject(start, end) - - @text_object("i", "W", no_move_handler=True) - def _inner_WORD(event: E) -> TextObject: - """ - Inner 'WORD': ciW and diW - """ - start, end = event.current_buffer.document.find_boundaries_of_current_word( - WORD=True - ) - return TextObject(start, end) - - @text_object("a", "W", no_move_handler=True) - def _a_WORD(event: E) -> TextObject: - """ - A 'WORD': caw and daw - """ - start, end = event.current_buffer.document.find_boundaries_of_current_word( - WORD=True, include_trailing_whitespace=True - ) - return TextObject(start, end) - - @text_object("a", "p", no_move_handler=True) - def _paragraph(event: E) -> TextObject: - """ - Auto paragraph. - """ - start = event.current_buffer.document.start_of_paragraph() - end = event.current_buffer.document.end_of_paragraph(count=event.arg) - return TextObject(start, end) - - @text_object("^") - def _start_of_line(event: E) -> TextObject: - """'c^', 'd^' and '^': Soft start of line, after whitespace.""" - return TextObject( - event.current_buffer.document.get_start_of_line_position( - after_whitespace=True - ) - ) - - @text_object("0") - def _hard_start_of_line(event: E) -> TextObject: - """ - 'c0', 'd0': Hard start of line, before whitespace. - (The move '0' key is implemented elsewhere, because a '0' could also change the `arg`.) - """ - return TextObject( - event.current_buffer.document.get_start_of_line_position( - after_whitespace=False - ) - ) - - def create_ci_ca_handles( - ci_start: str, ci_end: str, inner: bool, key: Optional[str] = None - ) -> None: - # TODO: 'dat', 'dit', (tags (like xml) - """ - Delete/Change string between this start and stop character. But keep these characters. - This implements all the ci", ci<, ci{, ci(, di", di<, ca", ca<, ... combinations. - """ - - def handler(event: E) -> TextObject: - if ci_start == ci_end: - # Quotes - start = event.current_buffer.document.find_backwards( - ci_start, in_current_line=False - ) - end = event.current_buffer.document.find(ci_end, in_current_line=False) - else: - # Brackets - start = event.current_buffer.document.find_enclosing_bracket_left( - ci_start, ci_end - ) - end = event.current_buffer.document.find_enclosing_bracket_right( - ci_start, ci_end - ) - - if start is not None and end is not None: - offset = 0 if inner else 1 - return TextObject(start + 1 - offset, end + offset) - else: - # Nothing found. - return TextObject(0) - - if key is None: - text_object("ai"[inner], ci_start, no_move_handler=True)(handler) - text_object("ai"[inner], ci_end, no_move_handler=True)(handler) - else: - text_object("ai"[inner], key, no_move_handler=True)(handler) - - for inner in (False, True): - for ci_start, ci_end in [ - ('"', '"'), - ("'", "'"), - ("`", "`"), - ("[", "]"), - ("<", ">"), - ("{", "}"), - ("(", ")"), - ]: - create_ci_ca_handles(ci_start, ci_end, inner) - - create_ci_ca_handles("(", ")", inner, "b") # 'dab', 'dib' - create_ci_ca_handles("{", "}", inner, "B") # 'daB', 'diB' - - @text_object("{") - def _previous_section(event: E) -> TextObject: - """ - Move to previous blank-line separated section. - Implements '{', 'c{', 'd{', 'y{' - """ - index = event.current_buffer.document.start_of_paragraph( - count=event.arg, before=True - ) - return TextObject(index) - - @text_object("}") - def _next_section(event: E) -> TextObject: - """ - Move to next blank-line separated section. - Implements '}', 'c}', 'd}', 'y}' - """ - index = event.current_buffer.document.end_of_paragraph( - count=event.arg, after=True - ) - return TextObject(index) - - @text_object("f", Keys.Any) - def _next_occurence(event: E) -> TextObject: - """ - Go to next occurrence of character. Typing 'fx' will move the - cursor to the next occurrence of character. 'x'. - """ - event.app.vi_state.last_character_find = CharacterFind(event.data, False) - match = event.current_buffer.document.find( - event.data, in_current_line=True, count=event.arg - ) - if match: - return TextObject(match, type=TextObjectType.INCLUSIVE) - else: - return TextObject(0) - - @text_object("F", Keys.Any) - def _previous_occurance(event: E) -> TextObject: - """ - Go to previous occurrence of character. Typing 'Fx' will move the - cursor to the previous occurrence of character. 'x'. - """ - event.app.vi_state.last_character_find = CharacterFind(event.data, True) - return TextObject( - event.current_buffer.document.find_backwards( - event.data, in_current_line=True, count=event.arg - ) - or 0 - ) - - @text_object("t", Keys.Any) - def _t(event: E) -> TextObject: - """ - Move right to the next occurrence of c, then one char backward. - """ - event.app.vi_state.last_character_find = CharacterFind(event.data, False) - match = event.current_buffer.document.find( - event.data, in_current_line=True, count=event.arg - ) - if match: - return TextObject(match - 1, type=TextObjectType.INCLUSIVE) - else: - return TextObject(0) - - @text_object("T", Keys.Any) - def _T(event: E) -> TextObject: - """ - Move left to the previous occurrence of c, then one char forward. - """ - event.app.vi_state.last_character_find = CharacterFind(event.data, True) - match = event.current_buffer.document.find_backwards( - event.data, in_current_line=True, count=event.arg - ) - return TextObject(match + 1 if match else 0) - - def repeat(reverse: bool) -> None: - """ - Create ',' and ';' commands. - """ - - @text_object("," if reverse else ";") - def _(event: E) -> TextObject: - """ - Repeat the last 'f'/'F'/'t'/'T' command. - """ - pos: Optional[int] = 0 - vi_state = event.app.vi_state - - type = TextObjectType.EXCLUSIVE - - if vi_state.last_character_find: - char = vi_state.last_character_find.character - backwards = vi_state.last_character_find.backwards - - if reverse: - backwards = not backwards - - if backwards: - pos = event.current_buffer.document.find_backwards( - char, in_current_line=True, count=event.arg - ) - else: - pos = event.current_buffer.document.find( - char, in_current_line=True, count=event.arg - ) - type = TextObjectType.INCLUSIVE - if pos: - return TextObject(pos, type=type) - else: - return TextObject(0) - - repeat(True) - repeat(False) - - @text_object("h") - @text_object("left") - def _left(event: E) -> TextObject: - """ - Implements 'ch', 'dh', 'h': Cursor left. - """ - return TextObject( - event.current_buffer.document.get_cursor_left_position(count=event.arg) - ) - - @text_object("j", no_move_handler=True, no_selection_handler=True) - # Note: We also need `no_selection_handler`, because we in - # selection mode, we prefer the other 'j' binding that keeps - # `buffer.preferred_column`. - def _down(event: E) -> TextObject: - """ - Implements 'cj', 'dj', 'j', ... Cursor up. - """ - return TextObject( - event.current_buffer.document.get_cursor_down_position(count=event.arg), - type=TextObjectType.LINEWISE, - ) - - @text_object("k", no_move_handler=True, no_selection_handler=True) - def _up(event: E) -> TextObject: - """ - Implements 'ck', 'dk', 'k', ... Cursor up. - """ - return TextObject( - event.current_buffer.document.get_cursor_up_position(count=event.arg), - type=TextObjectType.LINEWISE, - ) - - @text_object("l") - @text_object(" ") - @text_object("right") - def _right(event: E) -> TextObject: - """ - Implements 'cl', 'dl', 'l', 'c ', 'd ', ' '. Cursor right. - """ - return TextObject( - event.current_buffer.document.get_cursor_right_position(count=event.arg) - ) - - @text_object("H") - def _top_of_screen(event: E) -> TextObject: - """ - Moves to the start of the visible region. (Below the scroll offset.) - Implements 'cH', 'dH', 'H'. - """ - w = event.app.layout.current_window - b = event.current_buffer - - if w and w.render_info: - # When we find a Window that has BufferControl showing this window, - # move to the start of the visible area. - pos = ( - b.document.translate_row_col_to_index( - w.render_info.first_visible_line(after_scroll_offset=True), 0 - ) - - b.cursor_position - ) - - else: - # Otherwise, move to the start of the input. - pos = -len(b.document.text_before_cursor) - return TextObject(pos, type=TextObjectType.LINEWISE) - - @text_object("M") - def _middle_of_screen(event: E) -> TextObject: - """ - Moves cursor to the vertical center of the visible region. - Implements 'cM', 'dM', 'M'. - """ - w = event.app.layout.current_window - b = event.current_buffer - - if w and w.render_info: - # When we find a Window that has BufferControl showing this window, - # move to the center of the visible area. - pos = ( - b.document.translate_row_col_to_index( - w.render_info.center_visible_line(), 0 - ) - - b.cursor_position - ) - - else: - # Otherwise, move to the start of the input. - pos = -len(b.document.text_before_cursor) - return TextObject(pos, type=TextObjectType.LINEWISE) - - @text_object("L") - def _end_of_screen(event: E) -> TextObject: - """ - Moves to the end of the visible region. (Above the scroll offset.) - """ - w = event.app.layout.current_window - b = event.current_buffer - - if w and w.render_info: - # When we find a Window that has BufferControl showing this window, - # move to the end of the visible area. - pos = ( - b.document.translate_row_col_to_index( - w.render_info.last_visible_line(before_scroll_offset=True), 0 - ) - - b.cursor_position - ) - - else: - # Otherwise, move to the end of the input. - pos = len(b.document.text_after_cursor) - return TextObject(pos, type=TextObjectType.LINEWISE) - - @text_object("n", no_move_handler=True) - def _search_next(event: E) -> TextObject: - """ - Search next. - """ - buff = event.current_buffer - search_state = event.app.current_search_state - - cursor_position = buff.get_search_position( - search_state, include_current_position=False, count=event.arg - ) - return TextObject(cursor_position - buff.cursor_position) - - @handle("n", filter=vi_navigation_mode) - def _search_next2(event: E) -> None: - """ - Search next in navigation mode. (This goes through the history.) - """ - search_state = event.app.current_search_state - - event.current_buffer.apply_search( - search_state, include_current_position=False, count=event.arg - ) - - @text_object("N", no_move_handler=True) - def _search_previous(event: E) -> TextObject: - """ - Search previous. - """ - buff = event.current_buffer - search_state = event.app.current_search_state - - cursor_position = buff.get_search_position( - ~search_state, include_current_position=False, count=event.arg - ) - return TextObject(cursor_position - buff.cursor_position) - - @handle("N", filter=vi_navigation_mode) - def _search_previous2(event: E) -> None: - """ - Search previous in navigation mode. (This goes through the history.) - """ - search_state = event.app.current_search_state - - event.current_buffer.apply_search( - ~search_state, include_current_position=False, count=event.arg - ) - - @handle("z", "+", filter=vi_navigation_mode | vi_selection_mode) - @handle("z", "t", filter=vi_navigation_mode | vi_selection_mode) - @handle("z", "enter", filter=vi_navigation_mode | vi_selection_mode) - def _scroll_top(event: E) -> None: - """ - Scrolls the window to makes the current line the first line in the visible region. - """ - b = event.current_buffer - event.app.layout.current_window.vertical_scroll = b.document.cursor_position_row - - @handle("z", "-", filter=vi_navigation_mode | vi_selection_mode) - @handle("z", "b", filter=vi_navigation_mode | vi_selection_mode) - def _scroll_bottom(event: E) -> None: - """ - Scrolls the window to makes the current line the last line in the visible region. - """ - # We can safely set the scroll offset to zero; the Window will make - # sure that it scrolls at least enough to make the cursor visible - # again. - event.app.layout.current_window.vertical_scroll = 0 - - @handle("z", "z", filter=vi_navigation_mode | vi_selection_mode) - def _scroll_center(event: E) -> None: - """ - Center Window vertically around cursor. - """ - w = event.app.layout.current_window - b = event.current_buffer - - if w and w.render_info: - info = w.render_info - - # Calculate the offset that we need in order to position the row - # containing the cursor in the center. - scroll_height = info.window_height // 2 - - y = max(0, b.document.cursor_position_row - 1) - height = 0 - while y > 0: - line_height = info.get_height_for_line(y) - - if height + line_height < scroll_height: - height += line_height - y -= 1 - else: - break - - w.vertical_scroll = y - - @text_object("%") - def _goto_corresponding_bracket(event: E) -> TextObject: - """ - Implements 'c%', 'd%', '%, 'y%' (Move to corresponding bracket.) - If an 'arg' has been given, go this this % position in the file. - """ - buffer = event.current_buffer - - if event._arg: - # If 'arg' has been given, the meaning of % is to go to the 'x%' - # row in the file. - if 0 < event.arg <= 100: - absolute_index = buffer.document.translate_row_col_to_index( - int((event.arg * buffer.document.line_count - 1) / 100), 0 - ) - return TextObject( - absolute_index - buffer.document.cursor_position, - type=TextObjectType.LINEWISE, - ) - else: - return TextObject(0) # Do nothing. - - else: - # Move to the corresponding opening/closing bracket (()'s, []'s and {}'s). - match = buffer.document.find_matching_bracket_position() - if match: - return TextObject(match, type=TextObjectType.INCLUSIVE) - else: - return TextObject(0) - - @text_object("|") - def _to_column(event: E) -> TextObject: - """ - Move to the n-th column (you may specify the argument n by typing it on - number keys, for example, 20|). - """ - return TextObject( - event.current_buffer.document.get_column_cursor_position(event.arg - 1) - ) - - @text_object("g", "g") - def _goto_first_line(event: E) -> TextObject: - """ - Go to the start of the very first line. - Implements 'gg', 'cgg', 'ygg' - """ - d = event.current_buffer.document - - if event._arg: - # Move to the given line. - return TextObject( - d.translate_row_col_to_index(event.arg - 1, 0) - d.cursor_position, - type=TextObjectType.LINEWISE, - ) - else: - # Move to the top of the input. - return TextObject( - d.get_start_of_document_position(), type=TextObjectType.LINEWISE - ) - - @text_object("g", "_") - def _goto_last_line(event: E) -> TextObject: - """ - Go to last non-blank of line. - 'g_', 'cg_', 'yg_', etc.. - """ - return TextObject( - event.current_buffer.document.last_non_blank_of_current_line_position(), - type=TextObjectType.INCLUSIVE, - ) - - @text_object("g", "e") - def _ge(event: E) -> TextObject: - """ - Go to last character of previous word. - 'ge', 'cge', 'yge', etc.. - """ - prev_end = event.current_buffer.document.find_previous_word_ending( - count=event.arg - ) - return TextObject( - prev_end - 1 if prev_end is not None else 0, type=TextObjectType.INCLUSIVE - ) - - @text_object("g", "E") - def _gE(event: E) -> TextObject: - """ - Go to last character of previous WORD. - 'gE', 'cgE', 'ygE', etc.. - """ - prev_end = event.current_buffer.document.find_previous_word_ending( - count=event.arg, WORD=True - ) - return TextObject( - prev_end - 1 if prev_end is not None else 0, type=TextObjectType.INCLUSIVE - ) - - @text_object("g", "m") - def _gm(event: E) -> TextObject: - """ - Like g0, but half a screenwidth to the right. (Or as much as possible.) - """ - w = event.app.layout.current_window - buff = event.current_buffer - - if w and w.render_info: - width = w.render_info.window_width - start = buff.document.get_start_of_line_position(after_whitespace=False) - start += int(min(width / 2, len(buff.document.current_line))) - - return TextObject(start, type=TextObjectType.INCLUSIVE) - return TextObject(0) - - @text_object("G") - def _last_line(event: E) -> TextObject: - """ - Go to the end of the document. (If no arg has been given.) - """ - buf = event.current_buffer - return TextObject( - buf.document.translate_row_col_to_index(buf.document.line_count - 1, 0) - - buf.cursor_position, - type=TextObjectType.LINEWISE, - ) - - # - # *** Other *** - # - - @handle("G", filter=has_arg) - def _to_nth_history_line(event: E) -> None: - """ - If an argument is given, move to this line in the history. (for - example, 15G) - """ - event.current_buffer.go_to_history(event.arg - 1) - - for n in "123456789": - - @handle( - n, - filter=vi_navigation_mode - | vi_selection_mode - | vi_waiting_for_text_object_mode, - ) - def _arg(event: E) -> None: - """ - Always handle numberics in navigation mode as arg. - """ - event.append_to_arg_count(event.data) - - @handle( - "0", - filter=( - vi_navigation_mode | vi_selection_mode | vi_waiting_for_text_object_mode - ) - & has_arg, - ) - def _0_arg(event: E) -> None: - """ - Zero when an argument was already give. - """ - event.append_to_arg_count(event.data) - - @handle(Keys.Any, filter=vi_replace_mode) - def _insert_text(event: E) -> None: - """ - Insert data at cursor position. - """ - event.current_buffer.insert_text(event.data, overwrite=True) - - @handle(Keys.Any, filter=vi_replace_single_mode) - def _replace_single(event: E) -> None: - """ - Replace single character at cursor position. - """ - event.current_buffer.insert_text(event.data, overwrite=True) - event.current_buffer.cursor_position -= 1 - event.app.vi_state.input_mode = InputMode.NAVIGATION - - @handle( - Keys.Any, - filter=vi_insert_multiple_mode, - save_before=(lambda e: not e.is_repeat), - ) - def _insert_text_multiple_cursors(event: E) -> None: - """ - Insert data at multiple cursor positions at once. - (Usually a result of pressing 'I' or 'A' in block-selection mode.) - """ - buff = event.current_buffer - original_text = buff.text - - # Construct new text. - text = [] - p = 0 - - for p2 in buff.multiple_cursor_positions: - text.append(original_text[p:p2]) - text.append(event.data) - p = p2 - - text.append(original_text[p:]) - - # Shift all cursor positions. - new_cursor_positions = [ - pos + i + 1 for i, pos in enumerate(buff.multiple_cursor_positions) - ] - - # Set result. - buff.text = "".join(text) - buff.multiple_cursor_positions = new_cursor_positions - buff.cursor_position += 1 - - @handle("backspace", filter=vi_insert_multiple_mode) - def _delete_before_multiple_cursors(event: E) -> None: - """ - Backspace, using multiple cursors. - """ - buff = event.current_buffer - original_text = buff.text - - # Construct new text. - deleted_something = False - text = [] - p = 0 - - for p2 in buff.multiple_cursor_positions: - if p2 > 0 and original_text[p2 - 1] != "\n": # Don't delete across lines. - text.append(original_text[p : p2 - 1]) - deleted_something = True - else: - text.append(original_text[p:p2]) - p = p2 - - text.append(original_text[p:]) - - if deleted_something: - # Shift all cursor positions. - lengths = [len(part) for part in text[:-1]] - new_cursor_positions = list(accumulate(lengths)) - - # Set result. - buff.text = "".join(text) - buff.multiple_cursor_positions = new_cursor_positions - buff.cursor_position -= 1 - else: - event.app.output.bell() - - @handle("delete", filter=vi_insert_multiple_mode) - def _delete_after_multiple_cursors(event: E) -> None: - """ - Delete, using multiple cursors. - """ - buff = event.current_buffer - original_text = buff.text - - # Construct new text. - deleted_something = False - text = [] - new_cursor_positions = [] - p = 0 - - for p2 in buff.multiple_cursor_positions: - text.append(original_text[p:p2]) - if p2 >= len(original_text) or original_text[p2] == "\n": - # Don't delete across lines. - p = p2 - else: - p = p2 + 1 - deleted_something = True - - text.append(original_text[p:]) - - if deleted_something: - # Shift all cursor positions. - lengths = [len(part) for part in text[:-1]] - new_cursor_positions = list(accumulate(lengths)) - - # Set result. - buff.text = "".join(text) - buff.multiple_cursor_positions = new_cursor_positions - else: - event.app.output.bell() - - @handle("left", filter=vi_insert_multiple_mode) - def _left_multiple(event: E) -> None: - """ - Move all cursors to the left. - (But keep all cursors on the same line.) - """ - buff = event.current_buffer - new_positions = [] - - for p in buff.multiple_cursor_positions: - if buff.document.translate_index_to_position(p)[1] > 0: - p -= 1 - new_positions.append(p) - - buff.multiple_cursor_positions = new_positions - - if buff.document.cursor_position_col > 0: - buff.cursor_position -= 1 - - @handle("right", filter=vi_insert_multiple_mode) - def _right_multiple(event: E) -> None: - """ - Move all cursors to the right. - (But keep all cursors on the same line.) - """ - buff = event.current_buffer - new_positions = [] - - for p in buff.multiple_cursor_positions: - row, column = buff.document.translate_index_to_position(p) - if column < len(buff.document.lines[row]): - p += 1 - new_positions.append(p) - - buff.multiple_cursor_positions = new_positions - - if not buff.document.is_cursor_at_the_end_of_line: - buff.cursor_position += 1 - - @handle("up", filter=vi_insert_multiple_mode) - @handle("down", filter=vi_insert_multiple_mode) - def _updown_multiple(event: E) -> None: - """ - Ignore all up/down key presses when in multiple cursor mode. - """ - - @handle("c-x", "c-l", filter=vi_insert_mode) - def _complete_line(event: E) -> None: - """ - Pressing the ControlX - ControlL sequence in Vi mode does line - completion based on the other lines in the document and the history. - """ - event.current_buffer.start_history_lines_completion() - - @handle("c-x", "c-f", filter=vi_insert_mode) - def _complete_filename(event: E) -> None: - """ - Complete file names. - """ - # TODO - pass - - @handle("c-k", filter=vi_insert_mode | vi_replace_mode) - def _digraph(event: E) -> None: - """ - Go into digraph mode. - """ - event.app.vi_state.waiting_for_digraph = True - - @Condition - def digraph_symbol_1_given() -> bool: - return get_app().vi_state.digraph_symbol1 is not None - - @handle(Keys.Any, filter=vi_digraph_mode & ~digraph_symbol_1_given) - def _digraph1(event: E) -> None: - """ - First digraph symbol. - """ - event.app.vi_state.digraph_symbol1 = event.data - - @handle(Keys.Any, filter=vi_digraph_mode & digraph_symbol_1_given) - def _create_digraph(event: E) -> None: - """ - Insert digraph. - """ - try: - # Lookup. - code: Tuple[str, str] = ( - event.app.vi_state.digraph_symbol1 or "", - event.data, - ) - if code not in DIGRAPHS: - code = code[::-1] # Try reversing. - symbol = DIGRAPHS[code] - except KeyError: - # Unknown digraph. - event.app.output.bell() - else: - # Insert digraph. - overwrite = event.app.vi_state.input_mode == InputMode.REPLACE - event.current_buffer.insert_text(chr(symbol), overwrite=overwrite) - event.app.vi_state.waiting_for_digraph = False - finally: - event.app.vi_state.waiting_for_digraph = False - event.app.vi_state.digraph_symbol1 = None - - @handle("c-o", filter=vi_insert_mode | vi_replace_mode) - def _quick_normal_mode(event: E) -> None: - """ - Go into normal mode for one single action. - """ - event.app.vi_state.temporary_navigation_mode = True - - @handle("q", Keys.Any, filter=vi_navigation_mode & ~vi_recording_macro) - def _start_macro(event: E) -> None: - """ - Start recording macro. - """ - c = event.key_sequence[1].data - if c in vi_register_names: - vi_state = event.app.vi_state - - vi_state.recording_register = c - vi_state.current_recording = "" - - @handle("q", filter=vi_navigation_mode & vi_recording_macro) - def _stop_macro(event: E) -> None: - """ - Stop recording macro. - """ - vi_state = event.app.vi_state - - # Store and stop recording. - if vi_state.recording_register: - vi_state.named_registers[vi_state.recording_register] = ClipboardData( - vi_state.current_recording - ) - vi_state.recording_register = None - vi_state.current_recording = "" - - @handle("@", Keys.Any, filter=vi_navigation_mode, record_in_macro=False) - def _execute_macro(event: E) -> None: - """ - Execute macro. - - Notice that we pass `record_in_macro=False`. This ensures that the `@x` - keys don't appear in the recording itself. This function inserts the - body of the called macro back into the KeyProcessor, so these keys will - be added later on to the macro of their handlers have - `record_in_macro=True`. - """ - # Retrieve macro. - c = event.key_sequence[1].data - try: - macro = event.app.vi_state.named_registers[c] - except KeyError: - return - - # Expand macro (which is a string in the register), in individual keys. - # Use vt100 parser for this. - keys: List[KeyPress] = [] - - parser = Vt100Parser(keys.append) - parser.feed(macro.text) - parser.flush() - - # Now feed keys back to the input processor. - for _ in range(event.arg): - event.app.key_processor.feed_multiple(keys, first=True) - - return ConditionalKeyBindings(key_bindings, vi_mode) - - -def load_vi_search_bindings() -> KeyBindingsBase: - key_bindings = KeyBindings() - handle = key_bindings.add - from . import search - - @Condition - def search_buffer_is_empty() -> bool: - "Returns True when the search buffer is empty." - return get_app().current_buffer.text == "" - - # Vi-style forward search. - handle( - "/", - filter=(vi_navigation_mode | vi_selection_mode) & ~vi_search_direction_reversed, - )(search.start_forward_incremental_search) - handle( - "?", - filter=(vi_navigation_mode | vi_selection_mode) & vi_search_direction_reversed, - )(search.start_forward_incremental_search) - handle("c-s")(search.start_forward_incremental_search) - - # Vi-style backward search. - handle( - "?", - filter=(vi_navigation_mode | vi_selection_mode) & ~vi_search_direction_reversed, - )(search.start_reverse_incremental_search) - handle( - "/", - filter=(vi_navigation_mode | vi_selection_mode) & vi_search_direction_reversed, - )(search.start_reverse_incremental_search) - handle("c-r")(search.start_reverse_incremental_search) - - # Apply the search. (At the / or ? prompt.) - handle("enter", filter=is_searching)(search.accept_search) - - handle("c-r", filter=is_searching)(search.reverse_incremental_search) - handle("c-s", filter=is_searching)(search.forward_incremental_search) - - handle("c-c")(search.abort_search) - handle("c-g")(search.abort_search) - handle("backspace", filter=search_buffer_is_empty)(search.abort_search) - - # Handle escape. This should accept the search, just like readline. - # `abort_search` would be a meaningful alternative. - handle("escape")(search.accept_search) - - return ConditionalKeyBindings(key_bindings, vi_mode) + +from prompt_toolkit.application.current import get_app +from prompt_toolkit.buffer import Buffer, indent, reshape_text, unindent +from prompt_toolkit.clipboard import ClipboardData +from prompt_toolkit.document import Document +from prompt_toolkit.filters import ( + Always, + Condition, + Filter, + has_arg, + is_read_only, + is_searching, +) +from prompt_toolkit.filters.app import ( + in_paste_mode, + is_multiline, + vi_digraph_mode, + vi_insert_mode, + vi_insert_multiple_mode, + vi_mode, + vi_navigation_mode, + vi_recording_macro, + vi_replace_mode, + vi_replace_single_mode, + vi_search_direction_reversed, + vi_selection_mode, + vi_waiting_for_text_object_mode, +) +from prompt_toolkit.input.vt100_parser import Vt100Parser +from prompt_toolkit.key_binding.digraphs import DIGRAPHS +from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent +from prompt_toolkit.key_binding.vi_state import CharacterFind, InputMode +from prompt_toolkit.keys import Keys +from prompt_toolkit.search import SearchDirection +from prompt_toolkit.selection import PasteMode, SelectionState, SelectionType + +from ..key_bindings import ConditionalKeyBindings, KeyBindings, KeyBindingsBase +from .named_commands import get_by_name + +__all__ = [ + "load_vi_bindings", + "load_vi_search_bindings", +] + +E = KeyPressEvent + +ascii_lowercase = string.ascii_lowercase + +vi_register_names = ascii_lowercase + "0123456789" + + +class TextObjectType(Enum): + EXCLUSIVE = "EXCLUSIVE" + INCLUSIVE = "INCLUSIVE" + LINEWISE = "LINEWISE" + BLOCK = "BLOCK" + + +class TextObject: + """ + Return struct for functions wrapped in ``text_object``. + Both `start` and `end` are relative to the current cursor position. + """ + + def __init__( + self, start: int, end: int = 0, type: TextObjectType = TextObjectType.EXCLUSIVE + ): + + self.start = start + self.end = end + self.type = type + + @property + def selection_type(self) -> SelectionType: + if self.type == TextObjectType.LINEWISE: + return SelectionType.LINES + if self.type == TextObjectType.BLOCK: + return SelectionType.BLOCK + else: + return SelectionType.CHARACTERS + + def sorted(self) -> Tuple[int, int]: + """ + Return a (start, end) tuple where start <= end. + """ + if self.start < self.end: + return self.start, self.end + else: + return self.end, self.start + + def operator_range(self, document: Document) -> Tuple[int, int]: + """ + Return a (start, end) tuple with start <= end that indicates the range + operators should operate on. + `buffer` is used to get start and end of line positions. + + This should return something that can be used in a slice, so the `end` + position is *not* included. + """ + start, end = self.sorted() + doc = document + + if ( + self.type == TextObjectType.EXCLUSIVE + and doc.translate_index_to_position(end + doc.cursor_position)[1] == 0 + ): + # If the motion is exclusive and the end of motion is on the first + # column, the end position becomes end of previous line. + end -= 1 + if self.type == TextObjectType.INCLUSIVE: + end += 1 + if self.type == TextObjectType.LINEWISE: + # Select whole lines + row, col = doc.translate_index_to_position(start + doc.cursor_position) + start = doc.translate_row_col_to_index(row, 0) - doc.cursor_position + row, col = doc.translate_index_to_position(end + doc.cursor_position) + end = ( + doc.translate_row_col_to_index(row, len(doc.lines[row])) + - doc.cursor_position + ) + return start, end + + def get_line_numbers(self, buffer: Buffer) -> Tuple[int, int]: + """ + Return a (start_line, end_line) pair. + """ + # Get absolute cursor positions from the text object. + from_, to = self.operator_range(buffer.document) + from_ += buffer.cursor_position + to += buffer.cursor_position + + # Take the start of the lines. + from_, _ = buffer.document.translate_index_to_position(from_) + to, _ = buffer.document.translate_index_to_position(to) + + return from_, to + + def cut(self, buffer: Buffer) -> Tuple[Document, ClipboardData]: + """ + Turn text object into `ClipboardData` instance. + """ + from_, to = self.operator_range(buffer.document) + + from_ += buffer.cursor_position + to += buffer.cursor_position + + # For Vi mode, the SelectionState does include the upper position, + # while `self.operator_range` does not. So, go one to the left, unless + # we're in the line mode, then we don't want to risk going to the + # previous line, and missing one line in the selection. + if self.type != TextObjectType.LINEWISE: + to -= 1 + + document = Document( + buffer.text, + to, + SelectionState(original_cursor_position=from_, type=self.selection_type), + ) + + new_document, clipboard_data = document.cut_selection() + return new_document, clipboard_data + + +# Typevar for any text object function: +TextObjectFunction = Callable[[E], TextObject] +_TOF = TypeVar("_TOF", bound=TextObjectFunction) + + +def create_text_object_decorator( + key_bindings: KeyBindings, +) -> Callable[..., Callable[[_TOF], _TOF]]: + """ + Create a decorator that can be used to register Vi text object implementations. + """ + + def text_object_decorator( + *keys: Union[Keys, str], + filter: Filter = Always(), + no_move_handler: bool = False, + no_selection_handler: bool = False, + eager: bool = False, + ) -> Callable[[_TOF], _TOF]: + """ + Register a text object function. + + Usage:: + + @text_object('w', filter=..., no_move_handler=False) + def handler(event): + # Return a text object for this key. + return TextObject(...) + + :param no_move_handler: Disable the move handler in navigation mode. + (It's still active in selection mode.) + """ + + def decorator(text_object_func: _TOF) -> _TOF: + @key_bindings.add( + *keys, filter=vi_waiting_for_text_object_mode & filter, eager=eager + ) + def _apply_operator_to_text_object(event: E) -> None: + # Arguments are multiplied. + vi_state = event.app.vi_state + event._arg = str((vi_state.operator_arg or 1) * (event.arg or 1)) + + # Call the text object handler. + text_obj = text_object_func(event) + + # Get the operator function. + # (Should never be None here, given the + # `vi_waiting_for_text_object_mode` filter state.) + operator_func = vi_state.operator_func + + if text_obj is not None and operator_func is not None: + # Call the operator function with the text object. + operator_func(event, text_obj) + + # Clear operator. + event.app.vi_state.operator_func = None + event.app.vi_state.operator_arg = None + + # Register a move operation. (Doesn't need an operator.) + if not no_move_handler: + + @key_bindings.add( + *keys, + filter=~vi_waiting_for_text_object_mode + & filter + & vi_navigation_mode, + eager=eager, + ) + def _move_in_navigation_mode(event: E) -> None: + """ + Move handler for navigation mode. + """ + text_object = text_object_func(event) + event.current_buffer.cursor_position += text_object.start + + # Register a move selection operation. + if not no_selection_handler: + + @key_bindings.add( + *keys, + filter=~vi_waiting_for_text_object_mode + & filter + & vi_selection_mode, + eager=eager, + ) + def _move_in_selection_mode(event: E) -> None: + """ + Move handler for selection mode. + """ + text_object = text_object_func(event) + buff = event.current_buffer + selection_state = buff.selection_state + + if selection_state is None: + return # Should not happen, because of the `vi_selection_mode` filter. + + # When the text object has both a start and end position, like 'i(' or 'iw', + # Turn this into a selection, otherwise the cursor. + if text_object.end: + # Take selection positions from text object. + start, end = text_object.operator_range(buff.document) + start += buff.cursor_position + end += buff.cursor_position + + selection_state.original_cursor_position = start + buff.cursor_position = end + + # Take selection type from text object. + if text_object.type == TextObjectType.LINEWISE: + selection_state.type = SelectionType.LINES + else: + selection_state.type = SelectionType.CHARACTERS + else: + event.current_buffer.cursor_position += text_object.start + + # Make it possible to chain @text_object decorators. + return text_object_func + + return decorator + + return text_object_decorator + + +# Typevar for any operator function: +OperatorFunction = Callable[[E, TextObject], None] +_OF = TypeVar("_OF", bound=OperatorFunction) + + +def create_operator_decorator( + key_bindings: KeyBindings, +) -> Callable[..., Callable[[_OF], _OF]]: + """ + Create a decorator that can be used for registering Vi operators. + """ + + def operator_decorator( + *keys: Union[Keys, str], filter: Filter = Always(), eager: bool = False + ) -> Callable[[_OF], _OF]: + """ + Register a Vi operator. + + Usage:: + + @operator('d', filter=...) + def handler(event, text_object): + # Do something with the text object here. + """ + + def decorator(operator_func: _OF) -> _OF: + @key_bindings.add( + *keys, + filter=~vi_waiting_for_text_object_mode & filter & vi_navigation_mode, + eager=eager, + ) + def _operator_in_navigation(event: E) -> None: + """ + Handle operator in navigation mode. + """ + # When this key binding is matched, only set the operator + # function in the ViState. We should execute it after a text + # object has been received. + event.app.vi_state.operator_func = operator_func + event.app.vi_state.operator_arg = event.arg + + @key_bindings.add( + *keys, + filter=~vi_waiting_for_text_object_mode & filter & vi_selection_mode, + eager=eager, + ) + def _operator_in_selection(event: E) -> None: + """ + Handle operator in selection mode. + """ + buff = event.current_buffer + selection_state = buff.selection_state + + if selection_state is not None: + # Create text object from selection. + if selection_state.type == SelectionType.LINES: + text_obj_type = TextObjectType.LINEWISE + elif selection_state.type == SelectionType.BLOCK: + text_obj_type = TextObjectType.BLOCK + else: + text_obj_type = TextObjectType.INCLUSIVE + + text_object = TextObject( + selection_state.original_cursor_position - buff.cursor_position, + type=text_obj_type, + ) + + # Execute operator. + operator_func(event, text_object) + + # Quit selection mode. + buff.selection_state = None + + return operator_func + + return decorator + + return operator_decorator + + +def load_vi_bindings() -> KeyBindingsBase: + """ + Vi extensions. + + # Overview of Readline Vi commands: + # http://www.catonmat.net/download/bash-vi-editing-mode-cheat-sheet.pdf + """ + # Note: Some key bindings have the "~IsReadOnly()" filter added. This + # prevents the handler to be executed when the focus is on a + # read-only buffer. + # This is however only required for those that change the ViState to + # INSERT mode. The `Buffer` class itself throws the + # `EditReadOnlyBuffer` exception for any text operations which is + # handled correctly. There is no need to add "~IsReadOnly" to all key + # bindings that do text manipulation. + + key_bindings = KeyBindings() + handle = key_bindings.add + + # (Note: Always take the navigation bindings in read-only mode, even when + # ViState says different.) + + TransformFunction = Tuple[Tuple[str, ...], Filter, Callable[[str], str]] + + vi_transform_functions: List[TransformFunction] = [ + # Rot 13 transformation + ( + ("g", "?"), + Always(), + lambda string: codecs.encode(string, "rot_13"), + ), + # To lowercase + (("g", "u"), Always(), lambda string: string.lower()), + # To uppercase. + (("g", "U"), Always(), lambda string: string.upper()), + # Swap case. + (("g", "~"), Always(), lambda string: string.swapcase()), + ( + ("~",), + Condition(lambda: get_app().vi_state.tilde_operator), + lambda string: string.swapcase(), + ), + ] + + # Insert a character literally (quoted insert). + handle("c-v", filter=vi_insert_mode)(get_by_name("quoted-insert")) + + @handle("escape") + def _back_to_navigation(event: E) -> None: + """ + Escape goes to vi navigation mode. + """ + buffer = event.current_buffer + vi_state = event.app.vi_state + + if vi_state.input_mode in (InputMode.INSERT, InputMode.REPLACE): + buffer.cursor_position += buffer.document.get_cursor_left_position() + + vi_state.input_mode = InputMode.NAVIGATION + + if bool(buffer.selection_state): + buffer.exit_selection() + + @handle("k", filter=vi_selection_mode) + def _up_in_selection(event: E) -> None: + """ + Arrow up in selection mode. + """ + event.current_buffer.cursor_up(count=event.arg) + + @handle("j", filter=vi_selection_mode) + def _down_in_selection(event: E) -> None: + """ + Arrow down in selection mode. + """ + event.current_buffer.cursor_down(count=event.arg) + + @handle("up", filter=vi_navigation_mode) + @handle("c-p", filter=vi_navigation_mode) + def _up_in_navigation(event: E) -> None: + """ + Arrow up and ControlP in navigation mode go up. + """ + event.current_buffer.auto_up(count=event.arg) + + @handle("k", filter=vi_navigation_mode) + def _go_up(event: E) -> None: + """ + Go up, but if we enter a new history entry, move to the start of the + line. + """ + event.current_buffer.auto_up( + count=event.arg, go_to_start_of_line_if_history_changes=True + ) + + @handle("down", filter=vi_navigation_mode) + @handle("c-n", filter=vi_navigation_mode) + def _go_down(event: E) -> None: + """ + Arrow down and Control-N in navigation mode. + """ + event.current_buffer.auto_down(count=event.arg) + + @handle("j", filter=vi_navigation_mode) + def _go_down2(event: E) -> None: + """ + Go down, but if we enter a new history entry, go to the start of the line. + """ + event.current_buffer.auto_down( + count=event.arg, go_to_start_of_line_if_history_changes=True + ) + + @handle("backspace", filter=vi_navigation_mode) + def _go_left(event: E) -> None: + """ + In navigation-mode, move cursor. + """ + event.current_buffer.cursor_position += ( + event.current_buffer.document.get_cursor_left_position(count=event.arg) + ) + + @handle("c-n", filter=vi_insert_mode) + def _complete_next(event: E) -> None: + b = event.current_buffer + + if b.complete_state: + b.complete_next() + else: + b.start_completion(select_first=True) + + @handle("c-p", filter=vi_insert_mode) + def _complete_prev(event: E) -> None: + """ + Control-P: To previous completion. + """ + b = event.current_buffer + + if b.complete_state: + b.complete_previous() + else: + b.start_completion(select_last=True) + + @handle("c-g", filter=vi_insert_mode) + @handle("c-y", filter=vi_insert_mode) + def _accept_completion(event: E) -> None: + """ + Accept current completion. + """ + event.current_buffer.complete_state = None + + @handle("c-e", filter=vi_insert_mode) + def _cancel_completion(event: E) -> None: + """ + Cancel completion. Go back to originally typed text. + """ + event.current_buffer.cancel_completion() + + @Condition + def is_returnable() -> bool: + return get_app().current_buffer.is_returnable + + # In navigation mode, pressing enter will always return the input. + handle("enter", filter=vi_navigation_mode & is_returnable)( + get_by_name("accept-line") + ) + + # In insert mode, also accept input when enter is pressed, and the buffer + # has been marked as single line. + handle("enter", filter=is_returnable & ~is_multiline)(get_by_name("accept-line")) + + @handle("enter", filter=~is_returnable & vi_navigation_mode) + def _start_of_next_line(event: E) -> None: + """ + Go to the beginning of next line. + """ + b = event.current_buffer + b.cursor_down(count=event.arg) + b.cursor_position += b.document.get_start_of_line_position( + after_whitespace=True + ) + + # ** In navigation mode ** + + # List of navigation commands: http://hea-www.harvard.edu/~fine/Tech/vi.html + + @handle("insert", filter=vi_navigation_mode) + def _insert_mode(event: E) -> None: + """ + Pressing the Insert key. + """ + event.app.vi_state.input_mode = InputMode.INSERT + + @handle("insert", filter=vi_insert_mode) + def _navigation_mode(event: E) -> None: + """ + Pressing the Insert key. + """ + event.app.vi_state.input_mode = InputMode.NAVIGATION + + @handle("a", filter=vi_navigation_mode & ~is_read_only) + # ~IsReadOnly, because we want to stay in navigation mode for + # read-only buffers. + def _a(event: E) -> None: + event.current_buffer.cursor_position += ( + event.current_buffer.document.get_cursor_right_position() + ) + event.app.vi_state.input_mode = InputMode.INSERT + + @handle("A", filter=vi_navigation_mode & ~is_read_only) + def _A(event: E) -> None: + event.current_buffer.cursor_position += ( + event.current_buffer.document.get_end_of_line_position() + ) + event.app.vi_state.input_mode = InputMode.INSERT + + @handle("C", filter=vi_navigation_mode & ~is_read_only) + def _change_until_end_of_line(event: E) -> None: + """ + Change to end of line. + Same as 'c$' (which is implemented elsewhere.) + """ + buffer = event.current_buffer + + deleted = buffer.delete(count=buffer.document.get_end_of_line_position()) + event.app.clipboard.set_text(deleted) + event.app.vi_state.input_mode = InputMode.INSERT + + @handle("c", "c", filter=vi_navigation_mode & ~is_read_only) + @handle("S", filter=vi_navigation_mode & ~is_read_only) + def _change_current_line(event: E) -> None: # TODO: implement 'arg' + """ + Change current line + """ + buffer = event.current_buffer + + # We copy the whole line. + data = ClipboardData(buffer.document.current_line, SelectionType.LINES) + event.app.clipboard.set_data(data) + + # But we delete after the whitespace + buffer.cursor_position += buffer.document.get_start_of_line_position( + after_whitespace=True + ) + buffer.delete(count=buffer.document.get_end_of_line_position()) + event.app.vi_state.input_mode = InputMode.INSERT + + @handle("D", filter=vi_navigation_mode) + def _delete_until_end_of_line(event: E) -> None: + """ + Delete from cursor position until the end of the line. + """ + buffer = event.current_buffer + deleted = buffer.delete(count=buffer.document.get_end_of_line_position()) + event.app.clipboard.set_text(deleted) + + @handle("d", "d", filter=vi_navigation_mode) + def _delete_line(event: E) -> None: + """ + Delete line. (Or the following 'n' lines.) + """ + buffer = event.current_buffer + + # Split string in before/deleted/after text. + lines = buffer.document.lines + + before = "\n".join(lines[: buffer.document.cursor_position_row]) + deleted = "\n".join( + lines[ + buffer.document.cursor_position_row : buffer.document.cursor_position_row + + event.arg + ] + ) + after = "\n".join(lines[buffer.document.cursor_position_row + event.arg :]) + + # Set new text. + if before and after: + before = before + "\n" + + # Set text and cursor position. + buffer.document = Document( + text=before + after, + # Cursor At the start of the first 'after' line, after the leading whitespace. + cursor_position=len(before) + len(after) - len(after.lstrip(" ")), + ) + + # Set clipboard data + event.app.clipboard.set_data(ClipboardData(deleted, SelectionType.LINES)) + + @handle("x", filter=vi_selection_mode) + def _cut(event: E) -> None: + """ + Cut selection. + ('x' is not an operator.) + """ + clipboard_data = event.current_buffer.cut_selection() + event.app.clipboard.set_data(clipboard_data) + + @handle("i", filter=vi_navigation_mode & ~is_read_only) + def _i(event: E) -> None: + event.app.vi_state.input_mode = InputMode.INSERT + + @handle("I", filter=vi_navigation_mode & ~is_read_only) + def _I(event: E) -> None: + event.app.vi_state.input_mode = InputMode.INSERT + event.current_buffer.cursor_position += ( + event.current_buffer.document.get_start_of_line_position( + after_whitespace=True + ) + ) + + @Condition + def in_block_selection() -> bool: + buff = get_app().current_buffer + return bool( + buff.selection_state and buff.selection_state.type == SelectionType.BLOCK + ) + + @handle("I", filter=in_block_selection & ~is_read_only) + def insert_in_block_selection(event: E, after: bool = False) -> None: + """ + Insert in block selection mode. + """ + buff = event.current_buffer + + # Store all cursor positions. + positions = [] + + if after: + + def get_pos(from_to: Tuple[int, int]) -> int: + return from_to[1] + + else: + + def get_pos(from_to: Tuple[int, int]) -> int: + return from_to[0] + + for i, from_to in enumerate(buff.document.selection_ranges()): + positions.append(get_pos(from_to)) + if i == 0: + buff.cursor_position = get_pos(from_to) + + buff.multiple_cursor_positions = positions + + # Go to 'INSERT_MULTIPLE' mode. + event.app.vi_state.input_mode = InputMode.INSERT_MULTIPLE + buff.exit_selection() + + @handle("A", filter=in_block_selection & ~is_read_only) + def _append_after_block(event: E) -> None: + insert_in_block_selection(event, after=True) + + @handle("J", filter=vi_navigation_mode & ~is_read_only) + def _join(event: E) -> None: + """ + Join lines. + """ + for i in range(event.arg): + event.current_buffer.join_next_line() + + @handle("g", "J", filter=vi_navigation_mode & ~is_read_only) + def _join_nospace(event: E) -> None: + """ + Join lines without space. + """ + for i in range(event.arg): + event.current_buffer.join_next_line(separator="") + + @handle("J", filter=vi_selection_mode & ~is_read_only) + def _join_selection(event: E) -> None: + """ + Join selected lines. + """ + event.current_buffer.join_selected_lines() + + @handle("g", "J", filter=vi_selection_mode & ~is_read_only) + def _join_selection_nospace(event: E) -> None: + """ + Join selected lines without space. + """ + event.current_buffer.join_selected_lines(separator="") + + @handle("p", filter=vi_navigation_mode) + def _paste(event: E) -> None: + """ + Paste after + """ + event.current_buffer.paste_clipboard_data( + event.app.clipboard.get_data(), + count=event.arg, + paste_mode=PasteMode.VI_AFTER, + ) + + @handle("P", filter=vi_navigation_mode) + def _paste_before(event: E) -> None: + """ + Paste before + """ + event.current_buffer.paste_clipboard_data( + event.app.clipboard.get_data(), + count=event.arg, + paste_mode=PasteMode.VI_BEFORE, + ) + + @handle('"', Keys.Any, "p", filter=vi_navigation_mode) + def _paste_register(event: E) -> None: + """ + Paste from named register. + """ + c = event.key_sequence[1].data + if c in vi_register_names: + data = event.app.vi_state.named_registers.get(c) + if data: + event.current_buffer.paste_clipboard_data( + data, count=event.arg, paste_mode=PasteMode.VI_AFTER + ) + + @handle('"', Keys.Any, "P", filter=vi_navigation_mode) + def _paste_register_before(event: E) -> None: + """ + Paste (before) from named register. + """ + c = event.key_sequence[1].data + if c in vi_register_names: + data = event.app.vi_state.named_registers.get(c) + if data: + event.current_buffer.paste_clipboard_data( + data, count=event.arg, paste_mode=PasteMode.VI_BEFORE + ) + + @handle("r", filter=vi_navigation_mode) + def _replace(event: E) -> None: + """ + Go to 'replace-single'-mode. + """ + event.app.vi_state.input_mode = InputMode.REPLACE_SINGLE + + @handle("R", filter=vi_navigation_mode) + def _replace_mode(event: E) -> None: + """ + Go to 'replace'-mode. + """ + event.app.vi_state.input_mode = InputMode.REPLACE + + @handle("s", filter=vi_navigation_mode & ~is_read_only) + def _substitute(event: E) -> None: + """ + Substitute with new text + (Delete character(s) and go to insert mode.) + """ + text = event.current_buffer.delete(count=event.arg) + event.app.clipboard.set_text(text) + event.app.vi_state.input_mode = InputMode.INSERT + + @handle("u", filter=vi_navigation_mode, save_before=(lambda e: False)) + def _undo(event: E) -> None: + for i in range(event.arg): + event.current_buffer.undo() + + @handle("V", filter=vi_navigation_mode) + def _visual_line(event: E) -> None: + """ + Start lines selection. + """ + event.current_buffer.start_selection(selection_type=SelectionType.LINES) + + @handle("c-v", filter=vi_navigation_mode) + def _visual_block(event: E) -> None: + """ + Enter block selection mode. + """ + event.current_buffer.start_selection(selection_type=SelectionType.BLOCK) + + @handle("V", filter=vi_selection_mode) + def _visual_line2(event: E) -> None: + """ + Exit line selection mode, or go from non line selection mode to line + selection mode. + """ + selection_state = event.current_buffer.selection_state + + if selection_state is not None: + if selection_state.type != SelectionType.LINES: + selection_state.type = SelectionType.LINES + else: + event.current_buffer.exit_selection() + + @handle("v", filter=vi_navigation_mode) + def _visual(event: E) -> None: + """ + Enter character selection mode. + """ + event.current_buffer.start_selection(selection_type=SelectionType.CHARACTERS) + + @handle("v", filter=vi_selection_mode) + def _visual2(event: E) -> None: + """ + Exit character selection mode, or go from non-character-selection mode + to character selection mode. + """ + selection_state = event.current_buffer.selection_state + + if selection_state is not None: + if selection_state.type != SelectionType.CHARACTERS: + selection_state.type = SelectionType.CHARACTERS + else: + event.current_buffer.exit_selection() + + @handle("c-v", filter=vi_selection_mode) + def _visual_block2(event: E) -> None: + """ + Exit block selection mode, or go from non block selection mode to block + selection mode. + """ + selection_state = event.current_buffer.selection_state + + if selection_state is not None: + if selection_state.type != SelectionType.BLOCK: + selection_state.type = SelectionType.BLOCK + else: + event.current_buffer.exit_selection() + + @handle("a", "w", filter=vi_selection_mode) + @handle("a", "W", filter=vi_selection_mode) + def _visual_auto_word(event: E) -> None: + """ + Switch from visual linewise mode to visual characterwise mode. + """ + buffer = event.current_buffer + + if ( + buffer.selection_state + and buffer.selection_state.type == SelectionType.LINES + ): + buffer.selection_state.type = SelectionType.CHARACTERS + + @handle("x", filter=vi_navigation_mode) + def _delete(event: E) -> None: + """ + Delete character. + """ + buff = event.current_buffer + count = min(event.arg, len(buff.document.current_line_after_cursor)) + if count: + text = event.current_buffer.delete(count=count) + event.app.clipboard.set_text(text) + + @handle("X", filter=vi_navigation_mode) + def _delete_before_cursor(event: E) -> None: + buff = event.current_buffer + count = min(event.arg, len(buff.document.current_line_before_cursor)) + if count: + text = event.current_buffer.delete_before_cursor(count=count) + event.app.clipboard.set_text(text) + + @handle("y", "y", filter=vi_navigation_mode) + @handle("Y", filter=vi_navigation_mode) + def _yank_line(event: E) -> None: + """ + Yank the whole line. + """ + text = "\n".join(event.current_buffer.document.lines_from_current[: event.arg]) + event.app.clipboard.set_data(ClipboardData(text, SelectionType.LINES)) + + @handle("+", filter=vi_navigation_mode) + def _next_line(event: E) -> None: + """ + Move to first non whitespace of next line + """ + buffer = event.current_buffer + buffer.cursor_position += buffer.document.get_cursor_down_position( + count=event.arg + ) + buffer.cursor_position += buffer.document.get_start_of_line_position( + after_whitespace=True + ) + + @handle("-", filter=vi_navigation_mode) + def _prev_line(event: E) -> None: + """ + Move to first non whitespace of previous line + """ + buffer = event.current_buffer + buffer.cursor_position += buffer.document.get_cursor_up_position( + count=event.arg + ) + buffer.cursor_position += buffer.document.get_start_of_line_position( + after_whitespace=True + ) + + @handle(">", ">", filter=vi_navigation_mode) + def _indent(event: E) -> None: + """ + Indent lines. + """ + buffer = event.current_buffer + current_row = buffer.document.cursor_position_row + indent(buffer, current_row, current_row + event.arg) + + @handle("<", "<", filter=vi_navigation_mode) + def _unindent(event: E) -> None: + """ + Unindent lines. + """ + current_row = event.current_buffer.document.cursor_position_row + unindent(event.current_buffer, current_row, current_row + event.arg) + + @handle("O", filter=vi_navigation_mode & ~is_read_only) + def _open_above(event: E) -> None: + """ + Open line above and enter insertion mode + """ + event.current_buffer.insert_line_above(copy_margin=not in_paste_mode()) + event.app.vi_state.input_mode = InputMode.INSERT + + @handle("o", filter=vi_navigation_mode & ~is_read_only) + def _open_below(event: E) -> None: + """ + Open line below and enter insertion mode + """ + event.current_buffer.insert_line_below(copy_margin=not in_paste_mode()) + event.app.vi_state.input_mode = InputMode.INSERT + + @handle("~", filter=vi_navigation_mode) + def _reverse_case(event: E) -> None: + """ + Reverse case of current character and move cursor forward. + """ + buffer = event.current_buffer + c = buffer.document.current_char + + if c is not None and c != "\n": + buffer.insert_text(c.swapcase(), overwrite=True) + + @handle("g", "u", "u", filter=vi_navigation_mode & ~is_read_only) + def _lowercase_line(event: E) -> None: + """ + Lowercase current line. + """ + buff = event.current_buffer + buff.transform_current_line(lambda s: s.lower()) + + @handle("g", "U", "U", filter=vi_navigation_mode & ~is_read_only) + def _uppercase_line(event: E) -> None: + """ + Uppercase current line. + """ + buff = event.current_buffer + buff.transform_current_line(lambda s: s.upper()) + + @handle("g", "~", "~", filter=vi_navigation_mode & ~is_read_only) + def _swapcase_line(event: E) -> None: + """ + Swap case of the current line. + """ + buff = event.current_buffer + buff.transform_current_line(lambda s: s.swapcase()) + + @handle("#", filter=vi_navigation_mode) + def _prev_occurence(event: E) -> None: + """ + Go to previous occurrence of this word. + """ + b = event.current_buffer + search_state = event.app.current_search_state + + search_state.text = b.document.get_word_under_cursor() + search_state.direction = SearchDirection.BACKWARD + + b.apply_search(search_state, count=event.arg, include_current_position=False) + + @handle("*", filter=vi_navigation_mode) + def _next_occurance(event: E) -> None: + """ + Go to next occurrence of this word. + """ + b = event.current_buffer + search_state = event.app.current_search_state + + search_state.text = b.document.get_word_under_cursor() + search_state.direction = SearchDirection.FORWARD + + b.apply_search(search_state, count=event.arg, include_current_position=False) + + @handle("(", filter=vi_navigation_mode) + def _begin_of_sentence(event: E) -> None: + # TODO: go to begin of sentence. + # XXX: should become text_object. + pass + + @handle(")", filter=vi_navigation_mode) + def _end_of_sentence(event: E) -> None: + # TODO: go to end of sentence. + # XXX: should become text_object. + pass + + operator = create_operator_decorator(key_bindings) + text_object = create_text_object_decorator(key_bindings) + + @handle(Keys.Any, filter=vi_waiting_for_text_object_mode) + def _unknown_text_object(event: E) -> None: + """ + Unknown key binding while waiting for a text object. + """ + event.app.output.bell() + + # + # *** Operators *** + # + + def create_delete_and_change_operators( + delete_only: bool, with_register: bool = False + ) -> None: + """ + Delete and change operators. + + :param delete_only: Create an operator that deletes, but doesn't go to insert mode. + :param with_register: Copy the deleted text to this named register instead of the clipboard. + """ + handler_keys: Iterable[str] + if with_register: + handler_keys = ('"', Keys.Any, "cd"[delete_only]) + else: + handler_keys = "cd"[delete_only] + + @operator(*handler_keys, filter=~is_read_only) + def delete_or_change_operator(event: E, text_object: TextObject) -> None: + clipboard_data = None + buff = event.current_buffer + + if text_object: + new_document, clipboard_data = text_object.cut(buff) + buff.document = new_document + + # Set deleted/changed text to clipboard or named register. + if clipboard_data and clipboard_data.text: + if with_register: + reg_name = event.key_sequence[1].data + if reg_name in vi_register_names: + event.app.vi_state.named_registers[reg_name] = clipboard_data + else: + event.app.clipboard.set_data(clipboard_data) + + # Only go back to insert mode in case of 'change'. + if not delete_only: + event.app.vi_state.input_mode = InputMode.INSERT + + create_delete_and_change_operators(False, False) + create_delete_and_change_operators(False, True) + create_delete_and_change_operators(True, False) + create_delete_and_change_operators(True, True) + + def create_transform_handler( + filter: Filter, transform_func: Callable[[str], str], *a: str + ) -> None: + @operator(*a, filter=filter & ~is_read_only) + def _(event: E, text_object: TextObject) -> None: + """ + Apply transformation (uppercase, lowercase, rot13, swap case). + """ + buff = event.current_buffer + start, end = text_object.operator_range(buff.document) + + if start < end: + # Transform. + buff.transform_region( + buff.cursor_position + start, + buff.cursor_position + end, + transform_func, + ) + + # Move cursor + buff.cursor_position += text_object.end or text_object.start + + for k, f, func in vi_transform_functions: + create_transform_handler(f, func, *k) + + @operator("y") + def _yank(event: E, text_object: TextObject) -> None: + """ + Yank operator. (Copy text.) + """ + _, clipboard_data = text_object.cut(event.current_buffer) + if clipboard_data.text: + event.app.clipboard.set_data(clipboard_data) + + @operator('"', Keys.Any, "y") + def _yank_to_register(event: E, text_object: TextObject) -> None: + """ + Yank selection to named register. + """ + c = event.key_sequence[1].data + if c in vi_register_names: + _, clipboard_data = text_object.cut(event.current_buffer) + event.app.vi_state.named_registers[c] = clipboard_data + + @operator(">") + def _indent_text_object(event: E, text_object: TextObject) -> None: + """ + Indent. + """ + buff = event.current_buffer + from_, to = text_object.get_line_numbers(buff) + indent(buff, from_, to + 1, count=event.arg) + + @operator("<") + def _unindent_text_object(event: E, text_object: TextObject) -> None: + """ + Unindent. + """ + buff = event.current_buffer + from_, to = text_object.get_line_numbers(buff) + unindent(buff, from_, to + 1, count=event.arg) + + @operator("g", "q") + def _reshape(event: E, text_object: TextObject) -> None: + """ + Reshape text. + """ + buff = event.current_buffer + from_, to = text_object.get_line_numbers(buff) + reshape_text(buff, from_, to) + + # + # *** Text objects *** + # + + @text_object("b") + def _b(event: E) -> TextObject: + """ + Move one word or token left. + """ + return TextObject( + event.current_buffer.document.find_start_of_previous_word(count=event.arg) + or 0 + ) + + @text_object("B") + def _B(event: E) -> TextObject: + """ + Move one non-blank word left + """ + return TextObject( + event.current_buffer.document.find_start_of_previous_word( + count=event.arg, WORD=True + ) + or 0 + ) + + @text_object("$") + def _dollar(event: E) -> TextObject: + """ + 'c$', 'd$' and '$': Delete/change/move until end of line. + """ + return TextObject(event.current_buffer.document.get_end_of_line_position()) + + @text_object("w") + def _word_forward(event: E) -> TextObject: + """ + 'word' forward. 'cw', 'dw', 'w': Delete/change/move one word. + """ + return TextObject( + event.current_buffer.document.find_next_word_beginning(count=event.arg) + or event.current_buffer.document.get_end_of_document_position() + ) + + @text_object("W") + def _WORD_forward(event: E) -> TextObject: + """ + 'WORD' forward. 'cW', 'dW', 'W': Delete/change/move one WORD. + """ + return TextObject( + event.current_buffer.document.find_next_word_beginning( + count=event.arg, WORD=True + ) + or event.current_buffer.document.get_end_of_document_position() + ) + + @text_object("e") + def _end_of_word(event: E) -> TextObject: + """ + End of 'word': 'ce', 'de', 'e' + """ + end = event.current_buffer.document.find_next_word_ending(count=event.arg) + return TextObject(end - 1 if end else 0, type=TextObjectType.INCLUSIVE) + + @text_object("E") + def _end_of_WORD(event: E) -> TextObject: + """ + End of 'WORD': 'cE', 'dE', 'E' + """ + end = event.current_buffer.document.find_next_word_ending( + count=event.arg, WORD=True + ) + return TextObject(end - 1 if end else 0, type=TextObjectType.INCLUSIVE) + + @text_object("i", "w", no_move_handler=True) + def _inner_word(event: E) -> TextObject: + """ + Inner 'word': ciw and diw + """ + start, end = event.current_buffer.document.find_boundaries_of_current_word() + return TextObject(start, end) + + @text_object("a", "w", no_move_handler=True) + def _a_word(event: E) -> TextObject: + """ + A 'word': caw and daw + """ + start, end = event.current_buffer.document.find_boundaries_of_current_word( + include_trailing_whitespace=True + ) + return TextObject(start, end) + + @text_object("i", "W", no_move_handler=True) + def _inner_WORD(event: E) -> TextObject: + """ + Inner 'WORD': ciW and diW + """ + start, end = event.current_buffer.document.find_boundaries_of_current_word( + WORD=True + ) + return TextObject(start, end) + + @text_object("a", "W", no_move_handler=True) + def _a_WORD(event: E) -> TextObject: + """ + A 'WORD': caw and daw + """ + start, end = event.current_buffer.document.find_boundaries_of_current_word( + WORD=True, include_trailing_whitespace=True + ) + return TextObject(start, end) + + @text_object("a", "p", no_move_handler=True) + def _paragraph(event: E) -> TextObject: + """ + Auto paragraph. + """ + start = event.current_buffer.document.start_of_paragraph() + end = event.current_buffer.document.end_of_paragraph(count=event.arg) + return TextObject(start, end) + + @text_object("^") + def _start_of_line(event: E) -> TextObject: + """'c^', 'd^' and '^': Soft start of line, after whitespace.""" + return TextObject( + event.current_buffer.document.get_start_of_line_position( + after_whitespace=True + ) + ) + + @text_object("0") + def _hard_start_of_line(event: E) -> TextObject: + """ + 'c0', 'd0': Hard start of line, before whitespace. + (The move '0' key is implemented elsewhere, because a '0' could also change the `arg`.) + """ + return TextObject( + event.current_buffer.document.get_start_of_line_position( + after_whitespace=False + ) + ) + + def create_ci_ca_handles( + ci_start: str, ci_end: str, inner: bool, key: Optional[str] = None + ) -> None: + # TODO: 'dat', 'dit', (tags (like xml) + """ + Delete/Change string between this start and stop character. But keep these characters. + This implements all the ci", ci<, ci{, ci(, di", di<, ca", ca<, ... combinations. + """ + + def handler(event: E) -> TextObject: + if ci_start == ci_end: + # Quotes + start = event.current_buffer.document.find_backwards( + ci_start, in_current_line=False + ) + end = event.current_buffer.document.find(ci_end, in_current_line=False) + else: + # Brackets + start = event.current_buffer.document.find_enclosing_bracket_left( + ci_start, ci_end + ) + end = event.current_buffer.document.find_enclosing_bracket_right( + ci_start, ci_end + ) + + if start is not None and end is not None: + offset = 0 if inner else 1 + return TextObject(start + 1 - offset, end + offset) + else: + # Nothing found. + return TextObject(0) + + if key is None: + text_object("ai"[inner], ci_start, no_move_handler=True)(handler) + text_object("ai"[inner], ci_end, no_move_handler=True)(handler) + else: + text_object("ai"[inner], key, no_move_handler=True)(handler) + + for inner in (False, True): + for ci_start, ci_end in [ + ('"', '"'), + ("'", "'"), + ("`", "`"), + ("[", "]"), + ("<", ">"), + ("{", "}"), + ("(", ")"), + ]: + create_ci_ca_handles(ci_start, ci_end, inner) + + create_ci_ca_handles("(", ")", inner, "b") # 'dab', 'dib' + create_ci_ca_handles("{", "}", inner, "B") # 'daB', 'diB' + + @text_object("{") + def _previous_section(event: E) -> TextObject: + """ + Move to previous blank-line separated section. + Implements '{', 'c{', 'd{', 'y{' + """ + index = event.current_buffer.document.start_of_paragraph( + count=event.arg, before=True + ) + return TextObject(index) + + @text_object("}") + def _next_section(event: E) -> TextObject: + """ + Move to next blank-line separated section. + Implements '}', 'c}', 'd}', 'y}' + """ + index = event.current_buffer.document.end_of_paragraph( + count=event.arg, after=True + ) + return TextObject(index) + + @text_object("f", Keys.Any) + def _next_occurence(event: E) -> TextObject: + """ + Go to next occurrence of character. Typing 'fx' will move the + cursor to the next occurrence of character. 'x'. + """ + event.app.vi_state.last_character_find = CharacterFind(event.data, False) + match = event.current_buffer.document.find( + event.data, in_current_line=True, count=event.arg + ) + if match: + return TextObject(match, type=TextObjectType.INCLUSIVE) + else: + return TextObject(0) + + @text_object("F", Keys.Any) + def _previous_occurance(event: E) -> TextObject: + """ + Go to previous occurrence of character. Typing 'Fx' will move the + cursor to the previous occurrence of character. 'x'. + """ + event.app.vi_state.last_character_find = CharacterFind(event.data, True) + return TextObject( + event.current_buffer.document.find_backwards( + event.data, in_current_line=True, count=event.arg + ) + or 0 + ) + + @text_object("t", Keys.Any) + def _t(event: E) -> TextObject: + """ + Move right to the next occurrence of c, then one char backward. + """ + event.app.vi_state.last_character_find = CharacterFind(event.data, False) + match = event.current_buffer.document.find( + event.data, in_current_line=True, count=event.arg + ) + if match: + return TextObject(match - 1, type=TextObjectType.INCLUSIVE) + else: + return TextObject(0) + + @text_object("T", Keys.Any) + def _T(event: E) -> TextObject: + """ + Move left to the previous occurrence of c, then one char forward. + """ + event.app.vi_state.last_character_find = CharacterFind(event.data, True) + match = event.current_buffer.document.find_backwards( + event.data, in_current_line=True, count=event.arg + ) + return TextObject(match + 1 if match else 0) + + def repeat(reverse: bool) -> None: + """ + Create ',' and ';' commands. + """ + + @text_object("," if reverse else ";") + def _(event: E) -> TextObject: + """ + Repeat the last 'f'/'F'/'t'/'T' command. + """ + pos: Optional[int] = 0 + vi_state = event.app.vi_state + + type = TextObjectType.EXCLUSIVE + + if vi_state.last_character_find: + char = vi_state.last_character_find.character + backwards = vi_state.last_character_find.backwards + + if reverse: + backwards = not backwards + + if backwards: + pos = event.current_buffer.document.find_backwards( + char, in_current_line=True, count=event.arg + ) + else: + pos = event.current_buffer.document.find( + char, in_current_line=True, count=event.arg + ) + type = TextObjectType.INCLUSIVE + if pos: + return TextObject(pos, type=type) + else: + return TextObject(0) + + repeat(True) + repeat(False) + + @text_object("h") + @text_object("left") + def _left(event: E) -> TextObject: + """ + Implements 'ch', 'dh', 'h': Cursor left. + """ + return TextObject( + event.current_buffer.document.get_cursor_left_position(count=event.arg) + ) + + @text_object("j", no_move_handler=True, no_selection_handler=True) + # Note: We also need `no_selection_handler`, because we in + # selection mode, we prefer the other 'j' binding that keeps + # `buffer.preferred_column`. + def _down(event: E) -> TextObject: + """ + Implements 'cj', 'dj', 'j', ... Cursor up. + """ + return TextObject( + event.current_buffer.document.get_cursor_down_position(count=event.arg), + type=TextObjectType.LINEWISE, + ) + + @text_object("k", no_move_handler=True, no_selection_handler=True) + def _up(event: E) -> TextObject: + """ + Implements 'ck', 'dk', 'k', ... Cursor up. + """ + return TextObject( + event.current_buffer.document.get_cursor_up_position(count=event.arg), + type=TextObjectType.LINEWISE, + ) + + @text_object("l") + @text_object(" ") + @text_object("right") + def _right(event: E) -> TextObject: + """ + Implements 'cl', 'dl', 'l', 'c ', 'd ', ' '. Cursor right. + """ + return TextObject( + event.current_buffer.document.get_cursor_right_position(count=event.arg) + ) + + @text_object("H") + def _top_of_screen(event: E) -> TextObject: + """ + Moves to the start of the visible region. (Below the scroll offset.) + Implements 'cH', 'dH', 'H'. + """ + w = event.app.layout.current_window + b = event.current_buffer + + if w and w.render_info: + # When we find a Window that has BufferControl showing this window, + # move to the start of the visible area. + pos = ( + b.document.translate_row_col_to_index( + w.render_info.first_visible_line(after_scroll_offset=True), 0 + ) + - b.cursor_position + ) + + else: + # Otherwise, move to the start of the input. + pos = -len(b.document.text_before_cursor) + return TextObject(pos, type=TextObjectType.LINEWISE) + + @text_object("M") + def _middle_of_screen(event: E) -> TextObject: + """ + Moves cursor to the vertical center of the visible region. + Implements 'cM', 'dM', 'M'. + """ + w = event.app.layout.current_window + b = event.current_buffer + + if w and w.render_info: + # When we find a Window that has BufferControl showing this window, + # move to the center of the visible area. + pos = ( + b.document.translate_row_col_to_index( + w.render_info.center_visible_line(), 0 + ) + - b.cursor_position + ) + + else: + # Otherwise, move to the start of the input. + pos = -len(b.document.text_before_cursor) + return TextObject(pos, type=TextObjectType.LINEWISE) + + @text_object("L") + def _end_of_screen(event: E) -> TextObject: + """ + Moves to the end of the visible region. (Above the scroll offset.) + """ + w = event.app.layout.current_window + b = event.current_buffer + + if w and w.render_info: + # When we find a Window that has BufferControl showing this window, + # move to the end of the visible area. + pos = ( + b.document.translate_row_col_to_index( + w.render_info.last_visible_line(before_scroll_offset=True), 0 + ) + - b.cursor_position + ) + + else: + # Otherwise, move to the end of the input. + pos = len(b.document.text_after_cursor) + return TextObject(pos, type=TextObjectType.LINEWISE) + + @text_object("n", no_move_handler=True) + def _search_next(event: E) -> TextObject: + """ + Search next. + """ + buff = event.current_buffer + search_state = event.app.current_search_state + + cursor_position = buff.get_search_position( + search_state, include_current_position=False, count=event.arg + ) + return TextObject(cursor_position - buff.cursor_position) + + @handle("n", filter=vi_navigation_mode) + def _search_next2(event: E) -> None: + """ + Search next in navigation mode. (This goes through the history.) + """ + search_state = event.app.current_search_state + + event.current_buffer.apply_search( + search_state, include_current_position=False, count=event.arg + ) + + @text_object("N", no_move_handler=True) + def _search_previous(event: E) -> TextObject: + """ + Search previous. + """ + buff = event.current_buffer + search_state = event.app.current_search_state + + cursor_position = buff.get_search_position( + ~search_state, include_current_position=False, count=event.arg + ) + return TextObject(cursor_position - buff.cursor_position) + + @handle("N", filter=vi_navigation_mode) + def _search_previous2(event: E) -> None: + """ + Search previous in navigation mode. (This goes through the history.) + """ + search_state = event.app.current_search_state + + event.current_buffer.apply_search( + ~search_state, include_current_position=False, count=event.arg + ) + + @handle("z", "+", filter=vi_navigation_mode | vi_selection_mode) + @handle("z", "t", filter=vi_navigation_mode | vi_selection_mode) + @handle("z", "enter", filter=vi_navigation_mode | vi_selection_mode) + def _scroll_top(event: E) -> None: + """ + Scrolls the window to makes the current line the first line in the visible region. + """ + b = event.current_buffer + event.app.layout.current_window.vertical_scroll = b.document.cursor_position_row + + @handle("z", "-", filter=vi_navigation_mode | vi_selection_mode) + @handle("z", "b", filter=vi_navigation_mode | vi_selection_mode) + def _scroll_bottom(event: E) -> None: + """ + Scrolls the window to makes the current line the last line in the visible region. + """ + # We can safely set the scroll offset to zero; the Window will make + # sure that it scrolls at least enough to make the cursor visible + # again. + event.app.layout.current_window.vertical_scroll = 0 + + @handle("z", "z", filter=vi_navigation_mode | vi_selection_mode) + def _scroll_center(event: E) -> None: + """ + Center Window vertically around cursor. + """ + w = event.app.layout.current_window + b = event.current_buffer + + if w and w.render_info: + info = w.render_info + + # Calculate the offset that we need in order to position the row + # containing the cursor in the center. + scroll_height = info.window_height // 2 + + y = max(0, b.document.cursor_position_row - 1) + height = 0 + while y > 0: + line_height = info.get_height_for_line(y) + + if height + line_height < scroll_height: + height += line_height + y -= 1 + else: + break + + w.vertical_scroll = y + + @text_object("%") + def _goto_corresponding_bracket(event: E) -> TextObject: + """ + Implements 'c%', 'd%', '%, 'y%' (Move to corresponding bracket.) + If an 'arg' has been given, go this this % position in the file. + """ + buffer = event.current_buffer + + if event._arg: + # If 'arg' has been given, the meaning of % is to go to the 'x%' + # row in the file. + if 0 < event.arg <= 100: + absolute_index = buffer.document.translate_row_col_to_index( + int((event.arg * buffer.document.line_count - 1) / 100), 0 + ) + return TextObject( + absolute_index - buffer.document.cursor_position, + type=TextObjectType.LINEWISE, + ) + else: + return TextObject(0) # Do nothing. + + else: + # Move to the corresponding opening/closing bracket (()'s, []'s and {}'s). + match = buffer.document.find_matching_bracket_position() + if match: + return TextObject(match, type=TextObjectType.INCLUSIVE) + else: + return TextObject(0) + + @text_object("|") + def _to_column(event: E) -> TextObject: + """ + Move to the n-th column (you may specify the argument n by typing it on + number keys, for example, 20|). + """ + return TextObject( + event.current_buffer.document.get_column_cursor_position(event.arg - 1) + ) + + @text_object("g", "g") + def _goto_first_line(event: E) -> TextObject: + """ + Go to the start of the very first line. + Implements 'gg', 'cgg', 'ygg' + """ + d = event.current_buffer.document + + if event._arg: + # Move to the given line. + return TextObject( + d.translate_row_col_to_index(event.arg - 1, 0) - d.cursor_position, + type=TextObjectType.LINEWISE, + ) + else: + # Move to the top of the input. + return TextObject( + d.get_start_of_document_position(), type=TextObjectType.LINEWISE + ) + + @text_object("g", "_") + def _goto_last_line(event: E) -> TextObject: + """ + Go to last non-blank of line. + 'g_', 'cg_', 'yg_', etc.. + """ + return TextObject( + event.current_buffer.document.last_non_blank_of_current_line_position(), + type=TextObjectType.INCLUSIVE, + ) + + @text_object("g", "e") + def _ge(event: E) -> TextObject: + """ + Go to last character of previous word. + 'ge', 'cge', 'yge', etc.. + """ + prev_end = event.current_buffer.document.find_previous_word_ending( + count=event.arg + ) + return TextObject( + prev_end - 1 if prev_end is not None else 0, type=TextObjectType.INCLUSIVE + ) + + @text_object("g", "E") + def _gE(event: E) -> TextObject: + """ + Go to last character of previous WORD. + 'gE', 'cgE', 'ygE', etc.. + """ + prev_end = event.current_buffer.document.find_previous_word_ending( + count=event.arg, WORD=True + ) + return TextObject( + prev_end - 1 if prev_end is not None else 0, type=TextObjectType.INCLUSIVE + ) + + @text_object("g", "m") + def _gm(event: E) -> TextObject: + """ + Like g0, but half a screenwidth to the right. (Or as much as possible.) + """ + w = event.app.layout.current_window + buff = event.current_buffer + + if w and w.render_info: + width = w.render_info.window_width + start = buff.document.get_start_of_line_position(after_whitespace=False) + start += int(min(width / 2, len(buff.document.current_line))) + + return TextObject(start, type=TextObjectType.INCLUSIVE) + return TextObject(0) + + @text_object("G") + def _last_line(event: E) -> TextObject: + """ + Go to the end of the document. (If no arg has been given.) + """ + buf = event.current_buffer + return TextObject( + buf.document.translate_row_col_to_index(buf.document.line_count - 1, 0) + - buf.cursor_position, + type=TextObjectType.LINEWISE, + ) + + # + # *** Other *** + # + + @handle("G", filter=has_arg) + def _to_nth_history_line(event: E) -> None: + """ + If an argument is given, move to this line in the history. (for + example, 15G) + """ + event.current_buffer.go_to_history(event.arg - 1) + + for n in "123456789": + + @handle( + n, + filter=vi_navigation_mode + | vi_selection_mode + | vi_waiting_for_text_object_mode, + ) + def _arg(event: E) -> None: + """ + Always handle numberics in navigation mode as arg. + """ + event.append_to_arg_count(event.data) + + @handle( + "0", + filter=( + vi_navigation_mode | vi_selection_mode | vi_waiting_for_text_object_mode + ) + & has_arg, + ) + def _0_arg(event: E) -> None: + """ + Zero when an argument was already give. + """ + event.append_to_arg_count(event.data) + + @handle(Keys.Any, filter=vi_replace_mode) + def _insert_text(event: E) -> None: + """ + Insert data at cursor position. + """ + event.current_buffer.insert_text(event.data, overwrite=True) + + @handle(Keys.Any, filter=vi_replace_single_mode) + def _replace_single(event: E) -> None: + """ + Replace single character at cursor position. + """ + event.current_buffer.insert_text(event.data, overwrite=True) + event.current_buffer.cursor_position -= 1 + event.app.vi_state.input_mode = InputMode.NAVIGATION + + @handle( + Keys.Any, + filter=vi_insert_multiple_mode, + save_before=(lambda e: not e.is_repeat), + ) + def _insert_text_multiple_cursors(event: E) -> None: + """ + Insert data at multiple cursor positions at once. + (Usually a result of pressing 'I' or 'A' in block-selection mode.) + """ + buff = event.current_buffer + original_text = buff.text + + # Construct new text. + text = [] + p = 0 + + for p2 in buff.multiple_cursor_positions: + text.append(original_text[p:p2]) + text.append(event.data) + p = p2 + + text.append(original_text[p:]) + + # Shift all cursor positions. + new_cursor_positions = [ + pos + i + 1 for i, pos in enumerate(buff.multiple_cursor_positions) + ] + + # Set result. + buff.text = "".join(text) + buff.multiple_cursor_positions = new_cursor_positions + buff.cursor_position += 1 + + @handle("backspace", filter=vi_insert_multiple_mode) + def _delete_before_multiple_cursors(event: E) -> None: + """ + Backspace, using multiple cursors. + """ + buff = event.current_buffer + original_text = buff.text + + # Construct new text. + deleted_something = False + text = [] + p = 0 + + for p2 in buff.multiple_cursor_positions: + if p2 > 0 and original_text[p2 - 1] != "\n": # Don't delete across lines. + text.append(original_text[p : p2 - 1]) + deleted_something = True + else: + text.append(original_text[p:p2]) + p = p2 + + text.append(original_text[p:]) + + if deleted_something: + # Shift all cursor positions. + lengths = [len(part) for part in text[:-1]] + new_cursor_positions = list(accumulate(lengths)) + + # Set result. + buff.text = "".join(text) + buff.multiple_cursor_positions = new_cursor_positions + buff.cursor_position -= 1 + else: + event.app.output.bell() + + @handle("delete", filter=vi_insert_multiple_mode) + def _delete_after_multiple_cursors(event: E) -> None: + """ + Delete, using multiple cursors. + """ + buff = event.current_buffer + original_text = buff.text + + # Construct new text. + deleted_something = False + text = [] + new_cursor_positions = [] + p = 0 + + for p2 in buff.multiple_cursor_positions: + text.append(original_text[p:p2]) + if p2 >= len(original_text) or original_text[p2] == "\n": + # Don't delete across lines. + p = p2 + else: + p = p2 + 1 + deleted_something = True + + text.append(original_text[p:]) + + if deleted_something: + # Shift all cursor positions. + lengths = [len(part) for part in text[:-1]] + new_cursor_positions = list(accumulate(lengths)) + + # Set result. + buff.text = "".join(text) + buff.multiple_cursor_positions = new_cursor_positions + else: + event.app.output.bell() + + @handle("left", filter=vi_insert_multiple_mode) + def _left_multiple(event: E) -> None: + """ + Move all cursors to the left. + (But keep all cursors on the same line.) + """ + buff = event.current_buffer + new_positions = [] + + for p in buff.multiple_cursor_positions: + if buff.document.translate_index_to_position(p)[1] > 0: + p -= 1 + new_positions.append(p) + + buff.multiple_cursor_positions = new_positions + + if buff.document.cursor_position_col > 0: + buff.cursor_position -= 1 + + @handle("right", filter=vi_insert_multiple_mode) + def _right_multiple(event: E) -> None: + """ + Move all cursors to the right. + (But keep all cursors on the same line.) + """ + buff = event.current_buffer + new_positions = [] + + for p in buff.multiple_cursor_positions: + row, column = buff.document.translate_index_to_position(p) + if column < len(buff.document.lines[row]): + p += 1 + new_positions.append(p) + + buff.multiple_cursor_positions = new_positions + + if not buff.document.is_cursor_at_the_end_of_line: + buff.cursor_position += 1 + + @handle("up", filter=vi_insert_multiple_mode) + @handle("down", filter=vi_insert_multiple_mode) + def _updown_multiple(event: E) -> None: + """ + Ignore all up/down key presses when in multiple cursor mode. + """ + + @handle("c-x", "c-l", filter=vi_insert_mode) + def _complete_line(event: E) -> None: + """ + Pressing the ControlX - ControlL sequence in Vi mode does line + completion based on the other lines in the document and the history. + """ + event.current_buffer.start_history_lines_completion() + + @handle("c-x", "c-f", filter=vi_insert_mode) + def _complete_filename(event: E) -> None: + """ + Complete file names. + """ + # TODO + pass + + @handle("c-k", filter=vi_insert_mode | vi_replace_mode) + def _digraph(event: E) -> None: + """ + Go into digraph mode. + """ + event.app.vi_state.waiting_for_digraph = True + + @Condition + def digraph_symbol_1_given() -> bool: + return get_app().vi_state.digraph_symbol1 is not None + + @handle(Keys.Any, filter=vi_digraph_mode & ~digraph_symbol_1_given) + def _digraph1(event: E) -> None: + """ + First digraph symbol. + """ + event.app.vi_state.digraph_symbol1 = event.data + + @handle(Keys.Any, filter=vi_digraph_mode & digraph_symbol_1_given) + def _create_digraph(event: E) -> None: + """ + Insert digraph. + """ + try: + # Lookup. + code: Tuple[str, str] = ( + event.app.vi_state.digraph_symbol1 or "", + event.data, + ) + if code not in DIGRAPHS: + code = code[::-1] # Try reversing. + symbol = DIGRAPHS[code] + except KeyError: + # Unknown digraph. + event.app.output.bell() + else: + # Insert digraph. + overwrite = event.app.vi_state.input_mode == InputMode.REPLACE + event.current_buffer.insert_text(chr(symbol), overwrite=overwrite) + event.app.vi_state.waiting_for_digraph = False + finally: + event.app.vi_state.waiting_for_digraph = False + event.app.vi_state.digraph_symbol1 = None + + @handle("c-o", filter=vi_insert_mode | vi_replace_mode) + def _quick_normal_mode(event: E) -> None: + """ + Go into normal mode for one single action. + """ + event.app.vi_state.temporary_navigation_mode = True + + @handle("q", Keys.Any, filter=vi_navigation_mode & ~vi_recording_macro) + def _start_macro(event: E) -> None: + """ + Start recording macro. + """ + c = event.key_sequence[1].data + if c in vi_register_names: + vi_state = event.app.vi_state + + vi_state.recording_register = c + vi_state.current_recording = "" + + @handle("q", filter=vi_navigation_mode & vi_recording_macro) + def _stop_macro(event: E) -> None: + """ + Stop recording macro. + """ + vi_state = event.app.vi_state + + # Store and stop recording. + if vi_state.recording_register: + vi_state.named_registers[vi_state.recording_register] = ClipboardData( + vi_state.current_recording + ) + vi_state.recording_register = None + vi_state.current_recording = "" + + @handle("@", Keys.Any, filter=vi_navigation_mode, record_in_macro=False) + def _execute_macro(event: E) -> None: + """ + Execute macro. + + Notice that we pass `record_in_macro=False`. This ensures that the `@x` + keys don't appear in the recording itself. This function inserts the + body of the called macro back into the KeyProcessor, so these keys will + be added later on to the macro of their handlers have + `record_in_macro=True`. + """ + # Retrieve macro. + c = event.key_sequence[1].data + try: + macro = event.app.vi_state.named_registers[c] + except KeyError: + return + + # Expand macro (which is a string in the register), in individual keys. + # Use vt100 parser for this. + keys: List[KeyPress] = [] + + parser = Vt100Parser(keys.append) + parser.feed(macro.text) + parser.flush() + + # Now feed keys back to the input processor. + for _ in range(event.arg): + event.app.key_processor.feed_multiple(keys, first=True) + + return ConditionalKeyBindings(key_bindings, vi_mode) + + +def load_vi_search_bindings() -> KeyBindingsBase: + key_bindings = KeyBindings() + handle = key_bindings.add + from . import search + + @Condition + def search_buffer_is_empty() -> bool: + "Returns True when the search buffer is empty." + return get_app().current_buffer.text == "" + + # Vi-style forward search. + handle( + "/", + filter=(vi_navigation_mode | vi_selection_mode) & ~vi_search_direction_reversed, + )(search.start_forward_incremental_search) + handle( + "?", + filter=(vi_navigation_mode | vi_selection_mode) & vi_search_direction_reversed, + )(search.start_forward_incremental_search) + handle("c-s")(search.start_forward_incremental_search) + + # Vi-style backward search. + handle( + "?", + filter=(vi_navigation_mode | vi_selection_mode) & ~vi_search_direction_reversed, + )(search.start_reverse_incremental_search) + handle( + "/", + filter=(vi_navigation_mode | vi_selection_mode) & vi_search_direction_reversed, + )(search.start_reverse_incremental_search) + handle("c-r")(search.start_reverse_incremental_search) + + # Apply the search. (At the / or ? prompt.) + handle("enter", filter=is_searching)(search.accept_search) + + handle("c-r", filter=is_searching)(search.reverse_incremental_search) + handle("c-s", filter=is_searching)(search.forward_incremental_search) + + handle("c-c")(search.abort_search) + handle("c-g")(search.abort_search) + handle("backspace", filter=search_buffer_is_empty)(search.abort_search) + + # Handle escape. This should accept the search, just like readline. + # `abort_search` would be a meaningful alternative. + handle("escape")(search.accept_search) + + return ConditionalKeyBindings(key_bindings, vi_mode) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/defaults.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/defaults.py index 726335963c..baa5974333 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/defaults.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/defaults.py @@ -1,60 +1,60 @@ -""" -Default key bindings.:: - - key_bindings = load_key_bindings() - app = Application(key_bindings=key_bindings) -""" -from prompt_toolkit.filters import buffer_has_focus -from prompt_toolkit.key_binding.bindings.basic import load_basic_bindings -from prompt_toolkit.key_binding.bindings.cpr import load_cpr_bindings -from prompt_toolkit.key_binding.bindings.emacs import ( - load_emacs_bindings, - load_emacs_search_bindings, - load_emacs_shift_selection_bindings, -) -from prompt_toolkit.key_binding.bindings.mouse import load_mouse_bindings -from prompt_toolkit.key_binding.bindings.vi import ( - load_vi_bindings, - load_vi_search_bindings, -) -from prompt_toolkit.key_binding.key_bindings import ( - ConditionalKeyBindings, - KeyBindingsBase, - merge_key_bindings, -) - -__all__ = [ - "load_key_bindings", -] - - -def load_key_bindings() -> KeyBindingsBase: - """ - Create a KeyBindings object that contains the default key bindings. - """ - all_bindings = merge_key_bindings( - [ - # Load basic bindings. - load_basic_bindings(), - # Load emacs bindings. - load_emacs_bindings(), - load_emacs_search_bindings(), - load_emacs_shift_selection_bindings(), - # Load Vi bindings. - load_vi_bindings(), - load_vi_search_bindings(), - ] - ) - - return merge_key_bindings( - [ - # Make sure that the above key bindings are only active if the - # currently focused control is a `BufferControl`. For other controls, we - # don't want these key bindings to intervene. (This would break "ptterm" - # for instance, which handles 'Keys.Any' in the user control itself.) - ConditionalKeyBindings(all_bindings, buffer_has_focus), - # Active, even when no buffer has been focused. - load_mouse_bindings(), - load_cpr_bindings(), - ] - ) +""" +Default key bindings.:: + + key_bindings = load_key_bindings() + app = Application(key_bindings=key_bindings) +""" +from prompt_toolkit.filters import buffer_has_focus +from prompt_toolkit.key_binding.bindings.basic import load_basic_bindings +from prompt_toolkit.key_binding.bindings.cpr import load_cpr_bindings +from prompt_toolkit.key_binding.bindings.emacs import ( + load_emacs_bindings, + load_emacs_search_bindings, + load_emacs_shift_selection_bindings, +) +from prompt_toolkit.key_binding.bindings.mouse import load_mouse_bindings +from prompt_toolkit.key_binding.bindings.vi import ( + load_vi_bindings, + load_vi_search_bindings, +) +from prompt_toolkit.key_binding.key_bindings import ( + ConditionalKeyBindings, + KeyBindingsBase, + merge_key_bindings, +) + +__all__ = [ + "load_key_bindings", +] + + +def load_key_bindings() -> KeyBindingsBase: + """ + Create a KeyBindings object that contains the default key bindings. + """ + all_bindings = merge_key_bindings( + [ + # Load basic bindings. + load_basic_bindings(), + # Load emacs bindings. + load_emacs_bindings(), + load_emacs_search_bindings(), + load_emacs_shift_selection_bindings(), + # Load Vi bindings. + load_vi_bindings(), + load_vi_search_bindings(), + ] + ) + + return merge_key_bindings( + [ + # Make sure that the above key bindings are only active if the + # currently focused control is a `BufferControl`. For other controls, we + # don't want these key bindings to intervene. (This would break "ptterm" + # for instance, which handles 'Keys.Any' in the user control itself.) + ConditionalKeyBindings(all_bindings, buffer_has_focus), + # Active, even when no buffer has been focused. + load_mouse_bindings(), + load_cpr_bindings(), + ] + ) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/digraphs.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/digraphs.py index 70742fb90a..70606086aa 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/digraphs.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/digraphs.py @@ -1,1378 +1,1378 @@ -# encoding: utf-8 -""" -Vi Digraphs. -This is a list of special characters that can be inserted in Vi insert mode by -pressing Control-K followed by to normal characters. - -Taken from Neovim and translated to Python: -https://raw.githubusercontent.com/neovim/neovim/master/src/nvim/digraph.c -""" -from typing import Dict, Tuple - -__all__ = [ - "DIGRAPHS", -] - -# digraphs for Unicode from RFC1345 -# (also work for ISO-8859-1 aka latin1) -DIGRAPHS: Dict[Tuple[str, str], int] = { - ("N", "U"): 0x00, - ("S", "H"): 0x01, - ("S", "X"): 0x02, - ("E", "X"): 0x03, - ("E", "T"): 0x04, - ("E", "Q"): 0x05, - ("A", "K"): 0x06, - ("B", "L"): 0x07, - ("B", "S"): 0x08, - ("H", "T"): 0x09, - ("L", "F"): 0x0A, - ("V", "T"): 0x0B, - ("F", "F"): 0x0C, - ("C", "R"): 0x0D, - ("S", "O"): 0x0E, - ("S", "I"): 0x0F, - ("D", "L"): 0x10, - ("D", "1"): 0x11, - ("D", "2"): 0x12, - ("D", "3"): 0x13, - ("D", "4"): 0x14, - ("N", "K"): 0x15, - ("S", "Y"): 0x16, - ("E", "B"): 0x17, - ("C", "N"): 0x18, - ("E", "M"): 0x19, - ("S", "B"): 0x1A, - ("E", "C"): 0x1B, - ("F", "S"): 0x1C, - ("G", "S"): 0x1D, - ("R", "S"): 0x1E, - ("U", "S"): 0x1F, - ("S", "P"): 0x20, - ("N", "b"): 0x23, - ("D", "O"): 0x24, - ("A", "t"): 0x40, - ("<", "("): 0x5B, - ("/", "/"): 0x5C, - (")", ">"): 0x5D, - ("'", ">"): 0x5E, - ("'", "!"): 0x60, - ("(", "!"): 0x7B, - ("!", "!"): 0x7C, - ("!", ")"): 0x7D, - ("'", "?"): 0x7E, - ("D", "T"): 0x7F, - ("P", "A"): 0x80, - ("H", "O"): 0x81, - ("B", "H"): 0x82, - ("N", "H"): 0x83, - ("I", "N"): 0x84, - ("N", "L"): 0x85, - ("S", "A"): 0x86, - ("E", "S"): 0x87, - ("H", "S"): 0x88, - ("H", "J"): 0x89, - ("V", "S"): 0x8A, - ("P", "D"): 0x8B, - ("P", "U"): 0x8C, - ("R", "I"): 0x8D, - ("S", "2"): 0x8E, - ("S", "3"): 0x8F, - ("D", "C"): 0x90, - ("P", "1"): 0x91, - ("P", "2"): 0x92, - ("T", "S"): 0x93, - ("C", "C"): 0x94, - ("M", "W"): 0x95, - ("S", "G"): 0x96, - ("E", "G"): 0x97, - ("S", "S"): 0x98, - ("G", "C"): 0x99, - ("S", "C"): 0x9A, - ("C", "I"): 0x9B, - ("S", "T"): 0x9C, - ("O", "C"): 0x9D, - ("P", "M"): 0x9E, - ("A", "C"): 0x9F, - ("N", "S"): 0xA0, - ("!", "I"): 0xA1, - ("C", "t"): 0xA2, - ("P", "d"): 0xA3, - ("C", "u"): 0xA4, - ("Y", "e"): 0xA5, - ("B", "B"): 0xA6, - ("S", "E"): 0xA7, - ("'", ":"): 0xA8, - ("C", "o"): 0xA9, - ("-", "a"): 0xAA, - ("<", "<"): 0xAB, - ("N", "O"): 0xAC, - ("-", "-"): 0xAD, - ("R", "g"): 0xAE, - ("'", "m"): 0xAF, - ("D", "G"): 0xB0, - ("+", "-"): 0xB1, - ("2", "S"): 0xB2, - ("3", "S"): 0xB3, - ("'", "'"): 0xB4, - ("M", "y"): 0xB5, - ("P", "I"): 0xB6, - (".", "M"): 0xB7, - ("'", ","): 0xB8, - ("1", "S"): 0xB9, - ("-", "o"): 0xBA, - (">", ">"): 0xBB, - ("1", "4"): 0xBC, - ("1", "2"): 0xBD, - ("3", "4"): 0xBE, - ("?", "I"): 0xBF, - ("A", "!"): 0xC0, - ("A", "'"): 0xC1, - ("A", ">"): 0xC2, - ("A", "?"): 0xC3, - ("A", ":"): 0xC4, - ("A", "A"): 0xC5, - ("A", "E"): 0xC6, - ("C", ","): 0xC7, - ("E", "!"): 0xC8, - ("E", "'"): 0xC9, - ("E", ">"): 0xCA, - ("E", ":"): 0xCB, - ("I", "!"): 0xCC, - ("I", "'"): 0xCD, - ("I", ">"): 0xCE, - ("I", ":"): 0xCF, - ("D", "-"): 0xD0, - ("N", "?"): 0xD1, - ("O", "!"): 0xD2, - ("O", "'"): 0xD3, - ("O", ">"): 0xD4, - ("O", "?"): 0xD5, - ("O", ":"): 0xD6, - ("*", "X"): 0xD7, - ("O", "/"): 0xD8, - ("U", "!"): 0xD9, - ("U", "'"): 0xDA, - ("U", ">"): 0xDB, - ("U", ":"): 0xDC, - ("Y", "'"): 0xDD, - ("T", "H"): 0xDE, - ("s", "s"): 0xDF, - ("a", "!"): 0xE0, - ("a", "'"): 0xE1, - ("a", ">"): 0xE2, - ("a", "?"): 0xE3, - ("a", ":"): 0xE4, - ("a", "a"): 0xE5, - ("a", "e"): 0xE6, - ("c", ","): 0xE7, - ("e", "!"): 0xE8, - ("e", "'"): 0xE9, - ("e", ">"): 0xEA, - ("e", ":"): 0xEB, - ("i", "!"): 0xEC, - ("i", "'"): 0xED, - ("i", ">"): 0xEE, - ("i", ":"): 0xEF, - ("d", "-"): 0xF0, - ("n", "?"): 0xF1, - ("o", "!"): 0xF2, - ("o", "'"): 0xF3, - ("o", ">"): 0xF4, - ("o", "?"): 0xF5, - ("o", ":"): 0xF6, - ("-", ":"): 0xF7, - ("o", "/"): 0xF8, - ("u", "!"): 0xF9, - ("u", "'"): 0xFA, - ("u", ">"): 0xFB, - ("u", ":"): 0xFC, - ("y", "'"): 0xFD, - ("t", "h"): 0xFE, - ("y", ":"): 0xFF, - ("A", "-"): 0x0100, - ("a", "-"): 0x0101, - ("A", "("): 0x0102, - ("a", "("): 0x0103, - ("A", ";"): 0x0104, - ("a", ";"): 0x0105, - ("C", "'"): 0x0106, - ("c", "'"): 0x0107, - ("C", ">"): 0x0108, - ("c", ">"): 0x0109, - ("C", "."): 0x010A, - ("c", "."): 0x010B, - ("C", "<"): 0x010C, - ("c", "<"): 0x010D, - ("D", "<"): 0x010E, - ("d", "<"): 0x010F, - ("D", "/"): 0x0110, - ("d", "/"): 0x0111, - ("E", "-"): 0x0112, - ("e", "-"): 0x0113, - ("E", "("): 0x0114, - ("e", "("): 0x0115, - ("E", "."): 0x0116, - ("e", "."): 0x0117, - ("E", ";"): 0x0118, - ("e", ";"): 0x0119, - ("E", "<"): 0x011A, - ("e", "<"): 0x011B, - ("G", ">"): 0x011C, - ("g", ">"): 0x011D, - ("G", "("): 0x011E, - ("g", "("): 0x011F, - ("G", "."): 0x0120, - ("g", "."): 0x0121, - ("G", ","): 0x0122, - ("g", ","): 0x0123, - ("H", ">"): 0x0124, - ("h", ">"): 0x0125, - ("H", "/"): 0x0126, - ("h", "/"): 0x0127, - ("I", "?"): 0x0128, - ("i", "?"): 0x0129, - ("I", "-"): 0x012A, - ("i", "-"): 0x012B, - ("I", "("): 0x012C, - ("i", "("): 0x012D, - ("I", ";"): 0x012E, - ("i", ";"): 0x012F, - ("I", "."): 0x0130, - ("i", "."): 0x0131, - ("I", "J"): 0x0132, - ("i", "j"): 0x0133, - ("J", ">"): 0x0134, - ("j", ">"): 0x0135, - ("K", ","): 0x0136, - ("k", ","): 0x0137, - ("k", "k"): 0x0138, - ("L", "'"): 0x0139, - ("l", "'"): 0x013A, - ("L", ","): 0x013B, - ("l", ","): 0x013C, - ("L", "<"): 0x013D, - ("l", "<"): 0x013E, - ("L", "."): 0x013F, - ("l", "."): 0x0140, - ("L", "/"): 0x0141, - ("l", "/"): 0x0142, - ("N", "'"): 0x0143, - ("n", "'"): 0x0144, - ("N", ","): 0x0145, - ("n", ","): 0x0146, - ("N", "<"): 0x0147, - ("n", "<"): 0x0148, - ("'", "n"): 0x0149, - ("N", "G"): 0x014A, - ("n", "g"): 0x014B, - ("O", "-"): 0x014C, - ("o", "-"): 0x014D, - ("O", "("): 0x014E, - ("o", "("): 0x014F, - ("O", '"'): 0x0150, - ("o", '"'): 0x0151, - ("O", "E"): 0x0152, - ("o", "e"): 0x0153, - ("R", "'"): 0x0154, - ("r", "'"): 0x0155, - ("R", ","): 0x0156, - ("r", ","): 0x0157, - ("R", "<"): 0x0158, - ("r", "<"): 0x0159, - ("S", "'"): 0x015A, - ("s", "'"): 0x015B, - ("S", ">"): 0x015C, - ("s", ">"): 0x015D, - ("S", ","): 0x015E, - ("s", ","): 0x015F, - ("S", "<"): 0x0160, - ("s", "<"): 0x0161, - ("T", ","): 0x0162, - ("t", ","): 0x0163, - ("T", "<"): 0x0164, - ("t", "<"): 0x0165, - ("T", "/"): 0x0166, - ("t", "/"): 0x0167, - ("U", "?"): 0x0168, - ("u", "?"): 0x0169, - ("U", "-"): 0x016A, - ("u", "-"): 0x016B, - ("U", "("): 0x016C, - ("u", "("): 0x016D, - ("U", "0"): 0x016E, - ("u", "0"): 0x016F, - ("U", '"'): 0x0170, - ("u", '"'): 0x0171, - ("U", ";"): 0x0172, - ("u", ";"): 0x0173, - ("W", ">"): 0x0174, - ("w", ">"): 0x0175, - ("Y", ">"): 0x0176, - ("y", ">"): 0x0177, - ("Y", ":"): 0x0178, - ("Z", "'"): 0x0179, - ("z", "'"): 0x017A, - ("Z", "."): 0x017B, - ("z", "."): 0x017C, - ("Z", "<"): 0x017D, - ("z", "<"): 0x017E, - ("O", "9"): 0x01A0, - ("o", "9"): 0x01A1, - ("O", "I"): 0x01A2, - ("o", "i"): 0x01A3, - ("y", "r"): 0x01A6, - ("U", "9"): 0x01AF, - ("u", "9"): 0x01B0, - ("Z", "/"): 0x01B5, - ("z", "/"): 0x01B6, - ("E", "D"): 0x01B7, - ("A", "<"): 0x01CD, - ("a", "<"): 0x01CE, - ("I", "<"): 0x01CF, - ("i", "<"): 0x01D0, - ("O", "<"): 0x01D1, - ("o", "<"): 0x01D2, - ("U", "<"): 0x01D3, - ("u", "<"): 0x01D4, - ("A", "1"): 0x01DE, - ("a", "1"): 0x01DF, - ("A", "7"): 0x01E0, - ("a", "7"): 0x01E1, - ("A", "3"): 0x01E2, - ("a", "3"): 0x01E3, - ("G", "/"): 0x01E4, - ("g", "/"): 0x01E5, - ("G", "<"): 0x01E6, - ("g", "<"): 0x01E7, - ("K", "<"): 0x01E8, - ("k", "<"): 0x01E9, - ("O", ";"): 0x01EA, - ("o", ";"): 0x01EB, - ("O", "1"): 0x01EC, - ("o", "1"): 0x01ED, - ("E", "Z"): 0x01EE, - ("e", "z"): 0x01EF, - ("j", "<"): 0x01F0, - ("G", "'"): 0x01F4, - ("g", "'"): 0x01F5, - (";", "S"): 0x02BF, - ("'", "<"): 0x02C7, - ("'", "("): 0x02D8, - ("'", "."): 0x02D9, - ("'", "0"): 0x02DA, - ("'", ";"): 0x02DB, - ("'", '"'): 0x02DD, - ("A", "%"): 0x0386, - ("E", "%"): 0x0388, - ("Y", "%"): 0x0389, - ("I", "%"): 0x038A, - ("O", "%"): 0x038C, - ("U", "%"): 0x038E, - ("W", "%"): 0x038F, - ("i", "3"): 0x0390, - ("A", "*"): 0x0391, - ("B", "*"): 0x0392, - ("G", "*"): 0x0393, - ("D", "*"): 0x0394, - ("E", "*"): 0x0395, - ("Z", "*"): 0x0396, - ("Y", "*"): 0x0397, - ("H", "*"): 0x0398, - ("I", "*"): 0x0399, - ("K", "*"): 0x039A, - ("L", "*"): 0x039B, - ("M", "*"): 0x039C, - ("N", "*"): 0x039D, - ("C", "*"): 0x039E, - ("O", "*"): 0x039F, - ("P", "*"): 0x03A0, - ("R", "*"): 0x03A1, - ("S", "*"): 0x03A3, - ("T", "*"): 0x03A4, - ("U", "*"): 0x03A5, - ("F", "*"): 0x03A6, - ("X", "*"): 0x03A7, - ("Q", "*"): 0x03A8, - ("W", "*"): 0x03A9, - ("J", "*"): 0x03AA, - ("V", "*"): 0x03AB, - ("a", "%"): 0x03AC, - ("e", "%"): 0x03AD, - ("y", "%"): 0x03AE, - ("i", "%"): 0x03AF, - ("u", "3"): 0x03B0, - ("a", "*"): 0x03B1, - ("b", "*"): 0x03B2, - ("g", "*"): 0x03B3, - ("d", "*"): 0x03B4, - ("e", "*"): 0x03B5, - ("z", "*"): 0x03B6, - ("y", "*"): 0x03B7, - ("h", "*"): 0x03B8, - ("i", "*"): 0x03B9, - ("k", "*"): 0x03BA, - ("l", "*"): 0x03BB, - ("m", "*"): 0x03BC, - ("n", "*"): 0x03BD, - ("c", "*"): 0x03BE, - ("o", "*"): 0x03BF, - ("p", "*"): 0x03C0, - ("r", "*"): 0x03C1, - ("*", "s"): 0x03C2, - ("s", "*"): 0x03C3, - ("t", "*"): 0x03C4, - ("u", "*"): 0x03C5, - ("f", "*"): 0x03C6, - ("x", "*"): 0x03C7, - ("q", "*"): 0x03C8, - ("w", "*"): 0x03C9, - ("j", "*"): 0x03CA, - ("v", "*"): 0x03CB, - ("o", "%"): 0x03CC, - ("u", "%"): 0x03CD, - ("w", "%"): 0x03CE, - ("'", "G"): 0x03D8, - (",", "G"): 0x03D9, - ("T", "3"): 0x03DA, - ("t", "3"): 0x03DB, - ("M", "3"): 0x03DC, - ("m", "3"): 0x03DD, - ("K", "3"): 0x03DE, - ("k", "3"): 0x03DF, - ("P", "3"): 0x03E0, - ("p", "3"): 0x03E1, - ("'", "%"): 0x03F4, - ("j", "3"): 0x03F5, - ("I", "O"): 0x0401, - ("D", "%"): 0x0402, - ("G", "%"): 0x0403, - ("I", "E"): 0x0404, - ("D", "S"): 0x0405, - ("I", "I"): 0x0406, - ("Y", "I"): 0x0407, - ("J", "%"): 0x0408, - ("L", "J"): 0x0409, - ("N", "J"): 0x040A, - ("T", "s"): 0x040B, - ("K", "J"): 0x040C, - ("V", "%"): 0x040E, - ("D", "Z"): 0x040F, - ("A", "="): 0x0410, - ("B", "="): 0x0411, - ("V", "="): 0x0412, - ("G", "="): 0x0413, - ("D", "="): 0x0414, - ("E", "="): 0x0415, - ("Z", "%"): 0x0416, - ("Z", "="): 0x0417, - ("I", "="): 0x0418, - ("J", "="): 0x0419, - ("K", "="): 0x041A, - ("L", "="): 0x041B, - ("M", "="): 0x041C, - ("N", "="): 0x041D, - ("O", "="): 0x041E, - ("P", "="): 0x041F, - ("R", "="): 0x0420, - ("S", "="): 0x0421, - ("T", "="): 0x0422, - ("U", "="): 0x0423, - ("F", "="): 0x0424, - ("H", "="): 0x0425, - ("C", "="): 0x0426, - ("C", "%"): 0x0427, - ("S", "%"): 0x0428, - ("S", "c"): 0x0429, - ("=", '"'): 0x042A, - ("Y", "="): 0x042B, - ("%", '"'): 0x042C, - ("J", "E"): 0x042D, - ("J", "U"): 0x042E, - ("J", "A"): 0x042F, - ("a", "="): 0x0430, - ("b", "="): 0x0431, - ("v", "="): 0x0432, - ("g", "="): 0x0433, - ("d", "="): 0x0434, - ("e", "="): 0x0435, - ("z", "%"): 0x0436, - ("z", "="): 0x0437, - ("i", "="): 0x0438, - ("j", "="): 0x0439, - ("k", "="): 0x043A, - ("l", "="): 0x043B, - ("m", "="): 0x043C, - ("n", "="): 0x043D, - ("o", "="): 0x043E, - ("p", "="): 0x043F, - ("r", "="): 0x0440, - ("s", "="): 0x0441, - ("t", "="): 0x0442, - ("u", "="): 0x0443, - ("f", "="): 0x0444, - ("h", "="): 0x0445, - ("c", "="): 0x0446, - ("c", "%"): 0x0447, - ("s", "%"): 0x0448, - ("s", "c"): 0x0449, - ("=", "'"): 0x044A, - ("y", "="): 0x044B, - ("%", "'"): 0x044C, - ("j", "e"): 0x044D, - ("j", "u"): 0x044E, - ("j", "a"): 0x044F, - ("i", "o"): 0x0451, - ("d", "%"): 0x0452, - ("g", "%"): 0x0453, - ("i", "e"): 0x0454, - ("d", "s"): 0x0455, - ("i", "i"): 0x0456, - ("y", "i"): 0x0457, - ("j", "%"): 0x0458, - ("l", "j"): 0x0459, - ("n", "j"): 0x045A, - ("t", "s"): 0x045B, - ("k", "j"): 0x045C, - ("v", "%"): 0x045E, - ("d", "z"): 0x045F, - ("Y", "3"): 0x0462, - ("y", "3"): 0x0463, - ("O", "3"): 0x046A, - ("o", "3"): 0x046B, - ("F", "3"): 0x0472, - ("f", "3"): 0x0473, - ("V", "3"): 0x0474, - ("v", "3"): 0x0475, - ("C", "3"): 0x0480, - ("c", "3"): 0x0481, - ("G", "3"): 0x0490, - ("g", "3"): 0x0491, - ("A", "+"): 0x05D0, - ("B", "+"): 0x05D1, - ("G", "+"): 0x05D2, - ("D", "+"): 0x05D3, - ("H", "+"): 0x05D4, - ("W", "+"): 0x05D5, - ("Z", "+"): 0x05D6, - ("X", "+"): 0x05D7, - ("T", "j"): 0x05D8, - ("J", "+"): 0x05D9, - ("K", "%"): 0x05DA, - ("K", "+"): 0x05DB, - ("L", "+"): 0x05DC, - ("M", "%"): 0x05DD, - ("M", "+"): 0x05DE, - ("N", "%"): 0x05DF, - ("N", "+"): 0x05E0, - ("S", "+"): 0x05E1, - ("E", "+"): 0x05E2, - ("P", "%"): 0x05E3, - ("P", "+"): 0x05E4, - ("Z", "j"): 0x05E5, - ("Z", "J"): 0x05E6, - ("Q", "+"): 0x05E7, - ("R", "+"): 0x05E8, - ("S", "h"): 0x05E9, - ("T", "+"): 0x05EA, - (",", "+"): 0x060C, - (";", "+"): 0x061B, - ("?", "+"): 0x061F, - ("H", "'"): 0x0621, - ("a", "M"): 0x0622, - ("a", "H"): 0x0623, - ("w", "H"): 0x0624, - ("a", "h"): 0x0625, - ("y", "H"): 0x0626, - ("a", "+"): 0x0627, - ("b", "+"): 0x0628, - ("t", "m"): 0x0629, - ("t", "+"): 0x062A, - ("t", "k"): 0x062B, - ("g", "+"): 0x062C, - ("h", "k"): 0x062D, - ("x", "+"): 0x062E, - ("d", "+"): 0x062F, - ("d", "k"): 0x0630, - ("r", "+"): 0x0631, - ("z", "+"): 0x0632, - ("s", "+"): 0x0633, - ("s", "n"): 0x0634, - ("c", "+"): 0x0635, - ("d", "d"): 0x0636, - ("t", "j"): 0x0637, - ("z", "H"): 0x0638, - ("e", "+"): 0x0639, - ("i", "+"): 0x063A, - ("+", "+"): 0x0640, - ("f", "+"): 0x0641, - ("q", "+"): 0x0642, - ("k", "+"): 0x0643, - ("l", "+"): 0x0644, - ("m", "+"): 0x0645, - ("n", "+"): 0x0646, - ("h", "+"): 0x0647, - ("w", "+"): 0x0648, - ("j", "+"): 0x0649, - ("y", "+"): 0x064A, - (":", "+"): 0x064B, - ('"', "+"): 0x064C, - ("=", "+"): 0x064D, - ("/", "+"): 0x064E, - ("'", "+"): 0x064F, - ("1", "+"): 0x0650, - ("3", "+"): 0x0651, - ("0", "+"): 0x0652, - ("a", "S"): 0x0670, - ("p", "+"): 0x067E, - ("v", "+"): 0x06A4, - ("g", "f"): 0x06AF, - ("0", "a"): 0x06F0, - ("1", "a"): 0x06F1, - ("2", "a"): 0x06F2, - ("3", "a"): 0x06F3, - ("4", "a"): 0x06F4, - ("5", "a"): 0x06F5, - ("6", "a"): 0x06F6, - ("7", "a"): 0x06F7, - ("8", "a"): 0x06F8, - ("9", "a"): 0x06F9, - ("B", "."): 0x1E02, - ("b", "."): 0x1E03, - ("B", "_"): 0x1E06, - ("b", "_"): 0x1E07, - ("D", "."): 0x1E0A, - ("d", "."): 0x1E0B, - ("D", "_"): 0x1E0E, - ("d", "_"): 0x1E0F, - ("D", ","): 0x1E10, - ("d", ","): 0x1E11, - ("F", "."): 0x1E1E, - ("f", "."): 0x1E1F, - ("G", "-"): 0x1E20, - ("g", "-"): 0x1E21, - ("H", "."): 0x1E22, - ("h", "."): 0x1E23, - ("H", ":"): 0x1E26, - ("h", ":"): 0x1E27, - ("H", ","): 0x1E28, - ("h", ","): 0x1E29, - ("K", "'"): 0x1E30, - ("k", "'"): 0x1E31, - ("K", "_"): 0x1E34, - ("k", "_"): 0x1E35, - ("L", "_"): 0x1E3A, - ("l", "_"): 0x1E3B, - ("M", "'"): 0x1E3E, - ("m", "'"): 0x1E3F, - ("M", "."): 0x1E40, - ("m", "."): 0x1E41, - ("N", "."): 0x1E44, - ("n", "."): 0x1E45, - ("N", "_"): 0x1E48, - ("n", "_"): 0x1E49, - ("P", "'"): 0x1E54, - ("p", "'"): 0x1E55, - ("P", "."): 0x1E56, - ("p", "."): 0x1E57, - ("R", "."): 0x1E58, - ("r", "."): 0x1E59, - ("R", "_"): 0x1E5E, - ("r", "_"): 0x1E5F, - ("S", "."): 0x1E60, - ("s", "."): 0x1E61, - ("T", "."): 0x1E6A, - ("t", "."): 0x1E6B, - ("T", "_"): 0x1E6E, - ("t", "_"): 0x1E6F, - ("V", "?"): 0x1E7C, - ("v", "?"): 0x1E7D, - ("W", "!"): 0x1E80, - ("w", "!"): 0x1E81, - ("W", "'"): 0x1E82, - ("w", "'"): 0x1E83, - ("W", ":"): 0x1E84, - ("w", ":"): 0x1E85, - ("W", "."): 0x1E86, - ("w", "."): 0x1E87, - ("X", "."): 0x1E8A, - ("x", "."): 0x1E8B, - ("X", ":"): 0x1E8C, - ("x", ":"): 0x1E8D, - ("Y", "."): 0x1E8E, - ("y", "."): 0x1E8F, - ("Z", ">"): 0x1E90, - ("z", ">"): 0x1E91, - ("Z", "_"): 0x1E94, - ("z", "_"): 0x1E95, - ("h", "_"): 0x1E96, - ("t", ":"): 0x1E97, - ("w", "0"): 0x1E98, - ("y", "0"): 0x1E99, - ("A", "2"): 0x1EA2, - ("a", "2"): 0x1EA3, - ("E", "2"): 0x1EBA, - ("e", "2"): 0x1EBB, - ("E", "?"): 0x1EBC, - ("e", "?"): 0x1EBD, - ("I", "2"): 0x1EC8, - ("i", "2"): 0x1EC9, - ("O", "2"): 0x1ECE, - ("o", "2"): 0x1ECF, - ("U", "2"): 0x1EE6, - ("u", "2"): 0x1EE7, - ("Y", "!"): 0x1EF2, - ("y", "!"): 0x1EF3, - ("Y", "2"): 0x1EF6, - ("y", "2"): 0x1EF7, - ("Y", "?"): 0x1EF8, - ("y", "?"): 0x1EF9, - (";", "'"): 0x1F00, - (",", "'"): 0x1F01, - (";", "!"): 0x1F02, - (",", "!"): 0x1F03, - ("?", ";"): 0x1F04, - ("?", ","): 0x1F05, - ("!", ":"): 0x1F06, - ("?", ":"): 0x1F07, - ("1", "N"): 0x2002, - ("1", "M"): 0x2003, - ("3", "M"): 0x2004, - ("4", "M"): 0x2005, - ("6", "M"): 0x2006, - ("1", "T"): 0x2009, - ("1", "H"): 0x200A, - ("-", "1"): 0x2010, - ("-", "N"): 0x2013, - ("-", "M"): 0x2014, - ("-", "3"): 0x2015, - ("!", "2"): 0x2016, - ("=", "2"): 0x2017, - ("'", "6"): 0x2018, - ("'", "9"): 0x2019, - (".", "9"): 0x201A, - ("9", "'"): 0x201B, - ('"', "6"): 0x201C, - ('"', "9"): 0x201D, - (":", "9"): 0x201E, - ("9", '"'): 0x201F, - ("/", "-"): 0x2020, - ("/", "="): 0x2021, - (".", "."): 0x2025, - ("%", "0"): 0x2030, - ("1", "'"): 0x2032, - ("2", "'"): 0x2033, - ("3", "'"): 0x2034, - ("1", '"'): 0x2035, - ("2", '"'): 0x2036, - ("3", '"'): 0x2037, - ("C", "a"): 0x2038, - ("<", "1"): 0x2039, - (">", "1"): 0x203A, - (":", "X"): 0x203B, - ("'", "-"): 0x203E, - ("/", "f"): 0x2044, - ("0", "S"): 0x2070, - ("4", "S"): 0x2074, - ("5", "S"): 0x2075, - ("6", "S"): 0x2076, - ("7", "S"): 0x2077, - ("8", "S"): 0x2078, - ("9", "S"): 0x2079, - ("+", "S"): 0x207A, - ("-", "S"): 0x207B, - ("=", "S"): 0x207C, - ("(", "S"): 0x207D, - (")", "S"): 0x207E, - ("n", "S"): 0x207F, - ("0", "s"): 0x2080, - ("1", "s"): 0x2081, - ("2", "s"): 0x2082, - ("3", "s"): 0x2083, - ("4", "s"): 0x2084, - ("5", "s"): 0x2085, - ("6", "s"): 0x2086, - ("7", "s"): 0x2087, - ("8", "s"): 0x2088, - ("9", "s"): 0x2089, - ("+", "s"): 0x208A, - ("-", "s"): 0x208B, - ("=", "s"): 0x208C, - ("(", "s"): 0x208D, - (")", "s"): 0x208E, - ("L", "i"): 0x20A4, - ("P", "t"): 0x20A7, - ("W", "="): 0x20A9, - ("=", "e"): 0x20AC, # euro - ("E", "u"): 0x20AC, # euro - ("=", "R"): 0x20BD, # rouble - ("=", "P"): 0x20BD, # rouble - ("o", "C"): 0x2103, - ("c", "o"): 0x2105, - ("o", "F"): 0x2109, - ("N", "0"): 0x2116, - ("P", "O"): 0x2117, - ("R", "x"): 0x211E, - ("S", "M"): 0x2120, - ("T", "M"): 0x2122, - ("O", "m"): 0x2126, - ("A", "O"): 0x212B, - ("1", "3"): 0x2153, - ("2", "3"): 0x2154, - ("1", "5"): 0x2155, - ("2", "5"): 0x2156, - ("3", "5"): 0x2157, - ("4", "5"): 0x2158, - ("1", "6"): 0x2159, - ("5", "6"): 0x215A, - ("1", "8"): 0x215B, - ("3", "8"): 0x215C, - ("5", "8"): 0x215D, - ("7", "8"): 0x215E, - ("1", "R"): 0x2160, - ("2", "R"): 0x2161, - ("3", "R"): 0x2162, - ("4", "R"): 0x2163, - ("5", "R"): 0x2164, - ("6", "R"): 0x2165, - ("7", "R"): 0x2166, - ("8", "R"): 0x2167, - ("9", "R"): 0x2168, - ("a", "R"): 0x2169, - ("b", "R"): 0x216A, - ("c", "R"): 0x216B, - ("1", "r"): 0x2170, - ("2", "r"): 0x2171, - ("3", "r"): 0x2172, - ("4", "r"): 0x2173, - ("5", "r"): 0x2174, - ("6", "r"): 0x2175, - ("7", "r"): 0x2176, - ("8", "r"): 0x2177, - ("9", "r"): 0x2178, - ("a", "r"): 0x2179, - ("b", "r"): 0x217A, - ("c", "r"): 0x217B, - ("<", "-"): 0x2190, - ("-", "!"): 0x2191, - ("-", ">"): 0x2192, - ("-", "v"): 0x2193, - ("<", ">"): 0x2194, - ("U", "D"): 0x2195, - ("<", "="): 0x21D0, - ("=", ">"): 0x21D2, - ("=", "="): 0x21D4, - ("F", "A"): 0x2200, - ("d", "P"): 0x2202, - ("T", "E"): 0x2203, - ("/", "0"): 0x2205, - ("D", "E"): 0x2206, - ("N", "B"): 0x2207, - ("(", "-"): 0x2208, - ("-", ")"): 0x220B, - ("*", "P"): 0x220F, - ("+", "Z"): 0x2211, - ("-", "2"): 0x2212, - ("-", "+"): 0x2213, - ("*", "-"): 0x2217, - ("O", "b"): 0x2218, - ("S", "b"): 0x2219, - ("R", "T"): 0x221A, - ("0", "("): 0x221D, - ("0", "0"): 0x221E, - ("-", "L"): 0x221F, - ("-", "V"): 0x2220, - ("P", "P"): 0x2225, - ("A", "N"): 0x2227, - ("O", "R"): 0x2228, - ("(", "U"): 0x2229, - (")", "U"): 0x222A, - ("I", "n"): 0x222B, - ("D", "I"): 0x222C, - ("I", "o"): 0x222E, - (".", ":"): 0x2234, - (":", "."): 0x2235, - (":", "R"): 0x2236, - (":", ":"): 0x2237, - ("?", "1"): 0x223C, - ("C", "G"): 0x223E, - ("?", "-"): 0x2243, - ("?", "="): 0x2245, - ("?", "2"): 0x2248, - ("=", "?"): 0x224C, - ("H", "I"): 0x2253, - ("!", "="): 0x2260, - ("=", "3"): 0x2261, - ("=", "<"): 0x2264, - (">", "="): 0x2265, - ("<", "*"): 0x226A, - ("*", ">"): 0x226B, - ("!", "<"): 0x226E, - ("!", ">"): 0x226F, - ("(", "C"): 0x2282, - (")", "C"): 0x2283, - ("(", "_"): 0x2286, - (")", "_"): 0x2287, - ("0", "."): 0x2299, - ("0", "2"): 0x229A, - ("-", "T"): 0x22A5, - (".", "P"): 0x22C5, - (":", "3"): 0x22EE, - (".", "3"): 0x22EF, - ("E", "h"): 0x2302, - ("<", "7"): 0x2308, - (">", "7"): 0x2309, - ("7", "<"): 0x230A, - ("7", ">"): 0x230B, - ("N", "I"): 0x2310, - ("(", "A"): 0x2312, - ("T", "R"): 0x2315, - ("I", "u"): 0x2320, - ("I", "l"): 0x2321, - ("<", "/"): 0x2329, - ("/", ">"): 0x232A, - ("V", "s"): 0x2423, - ("1", "h"): 0x2440, - ("3", "h"): 0x2441, - ("2", "h"): 0x2442, - ("4", "h"): 0x2443, - ("1", "j"): 0x2446, - ("2", "j"): 0x2447, - ("3", "j"): 0x2448, - ("4", "j"): 0x2449, - ("1", "."): 0x2488, - ("2", "."): 0x2489, - ("3", "."): 0x248A, - ("4", "."): 0x248B, - ("5", "."): 0x248C, - ("6", "."): 0x248D, - ("7", "."): 0x248E, - ("8", "."): 0x248F, - ("9", "."): 0x2490, - ("h", "h"): 0x2500, - ("H", "H"): 0x2501, - ("v", "v"): 0x2502, - ("V", "V"): 0x2503, - ("3", "-"): 0x2504, - ("3", "_"): 0x2505, - ("3", "!"): 0x2506, - ("3", "/"): 0x2507, - ("4", "-"): 0x2508, - ("4", "_"): 0x2509, - ("4", "!"): 0x250A, - ("4", "/"): 0x250B, - ("d", "r"): 0x250C, - ("d", "R"): 0x250D, - ("D", "r"): 0x250E, - ("D", "R"): 0x250F, - ("d", "l"): 0x2510, - ("d", "L"): 0x2511, - ("D", "l"): 0x2512, - ("L", "D"): 0x2513, - ("u", "r"): 0x2514, - ("u", "R"): 0x2515, - ("U", "r"): 0x2516, - ("U", "R"): 0x2517, - ("u", "l"): 0x2518, - ("u", "L"): 0x2519, - ("U", "l"): 0x251A, - ("U", "L"): 0x251B, - ("v", "r"): 0x251C, - ("v", "R"): 0x251D, - ("V", "r"): 0x2520, - ("V", "R"): 0x2523, - ("v", "l"): 0x2524, - ("v", "L"): 0x2525, - ("V", "l"): 0x2528, - ("V", "L"): 0x252B, - ("d", "h"): 0x252C, - ("d", "H"): 0x252F, - ("D", "h"): 0x2530, - ("D", "H"): 0x2533, - ("u", "h"): 0x2534, - ("u", "H"): 0x2537, - ("U", "h"): 0x2538, - ("U", "H"): 0x253B, - ("v", "h"): 0x253C, - ("v", "H"): 0x253F, - ("V", "h"): 0x2542, - ("V", "H"): 0x254B, - ("F", "D"): 0x2571, - ("B", "D"): 0x2572, - ("T", "B"): 0x2580, - ("L", "B"): 0x2584, - ("F", "B"): 0x2588, - ("l", "B"): 0x258C, - ("R", "B"): 0x2590, - (".", "S"): 0x2591, - (":", "S"): 0x2592, - ("?", "S"): 0x2593, - ("f", "S"): 0x25A0, - ("O", "S"): 0x25A1, - ("R", "O"): 0x25A2, - ("R", "r"): 0x25A3, - ("R", "F"): 0x25A4, - ("R", "Y"): 0x25A5, - ("R", "H"): 0x25A6, - ("R", "Z"): 0x25A7, - ("R", "K"): 0x25A8, - ("R", "X"): 0x25A9, - ("s", "B"): 0x25AA, - ("S", "R"): 0x25AC, - ("O", "r"): 0x25AD, - ("U", "T"): 0x25B2, - ("u", "T"): 0x25B3, - ("P", "R"): 0x25B6, - ("T", "r"): 0x25B7, - ("D", "t"): 0x25BC, - ("d", "T"): 0x25BD, - ("P", "L"): 0x25C0, - ("T", "l"): 0x25C1, - ("D", "b"): 0x25C6, - ("D", "w"): 0x25C7, - ("L", "Z"): 0x25CA, - ("0", "m"): 0x25CB, - ("0", "o"): 0x25CE, - ("0", "M"): 0x25CF, - ("0", "L"): 0x25D0, - ("0", "R"): 0x25D1, - ("S", "n"): 0x25D8, - ("I", "c"): 0x25D9, - ("F", "d"): 0x25E2, - ("B", "d"): 0x25E3, - ("*", "2"): 0x2605, - ("*", "1"): 0x2606, - ("<", "H"): 0x261C, - (">", "H"): 0x261E, - ("0", "u"): 0x263A, - ("0", "U"): 0x263B, - ("S", "U"): 0x263C, - ("F", "m"): 0x2640, - ("M", "l"): 0x2642, - ("c", "S"): 0x2660, - ("c", "H"): 0x2661, - ("c", "D"): 0x2662, - ("c", "C"): 0x2663, - ("M", "d"): 0x2669, - ("M", "8"): 0x266A, - ("M", "2"): 0x266B, - ("M", "b"): 0x266D, - ("M", "x"): 0x266E, - ("M", "X"): 0x266F, - ("O", "K"): 0x2713, - ("X", "X"): 0x2717, - ("-", "X"): 0x2720, - ("I", "S"): 0x3000, - (",", "_"): 0x3001, - (".", "_"): 0x3002, - ("+", '"'): 0x3003, - ("+", "_"): 0x3004, - ("*", "_"): 0x3005, - (";", "_"): 0x3006, - ("0", "_"): 0x3007, - ("<", "+"): 0x300A, - (">", "+"): 0x300B, - ("<", "'"): 0x300C, - (">", "'"): 0x300D, - ("<", '"'): 0x300E, - (">", '"'): 0x300F, - ("(", '"'): 0x3010, - (")", '"'): 0x3011, - ("=", "T"): 0x3012, - ("=", "_"): 0x3013, - ("(", "'"): 0x3014, - (")", "'"): 0x3015, - ("(", "I"): 0x3016, - (")", "I"): 0x3017, - ("-", "?"): 0x301C, - ("A", "5"): 0x3041, - ("a", "5"): 0x3042, - ("I", "5"): 0x3043, - ("i", "5"): 0x3044, - ("U", "5"): 0x3045, - ("u", "5"): 0x3046, - ("E", "5"): 0x3047, - ("e", "5"): 0x3048, - ("O", "5"): 0x3049, - ("o", "5"): 0x304A, - ("k", "a"): 0x304B, - ("g", "a"): 0x304C, - ("k", "i"): 0x304D, - ("g", "i"): 0x304E, - ("k", "u"): 0x304F, - ("g", "u"): 0x3050, - ("k", "e"): 0x3051, - ("g", "e"): 0x3052, - ("k", "o"): 0x3053, - ("g", "o"): 0x3054, - ("s", "a"): 0x3055, - ("z", "a"): 0x3056, - ("s", "i"): 0x3057, - ("z", "i"): 0x3058, - ("s", "u"): 0x3059, - ("z", "u"): 0x305A, - ("s", "e"): 0x305B, - ("z", "e"): 0x305C, - ("s", "o"): 0x305D, - ("z", "o"): 0x305E, - ("t", "a"): 0x305F, - ("d", "a"): 0x3060, - ("t", "i"): 0x3061, - ("d", "i"): 0x3062, - ("t", "U"): 0x3063, - ("t", "u"): 0x3064, - ("d", "u"): 0x3065, - ("t", "e"): 0x3066, - ("d", "e"): 0x3067, - ("t", "o"): 0x3068, - ("d", "o"): 0x3069, - ("n", "a"): 0x306A, - ("n", "i"): 0x306B, - ("n", "u"): 0x306C, - ("n", "e"): 0x306D, - ("n", "o"): 0x306E, - ("h", "a"): 0x306F, - ("b", "a"): 0x3070, - ("p", "a"): 0x3071, - ("h", "i"): 0x3072, - ("b", "i"): 0x3073, - ("p", "i"): 0x3074, - ("h", "u"): 0x3075, - ("b", "u"): 0x3076, - ("p", "u"): 0x3077, - ("h", "e"): 0x3078, - ("b", "e"): 0x3079, - ("p", "e"): 0x307A, - ("h", "o"): 0x307B, - ("b", "o"): 0x307C, - ("p", "o"): 0x307D, - ("m", "a"): 0x307E, - ("m", "i"): 0x307F, - ("m", "u"): 0x3080, - ("m", "e"): 0x3081, - ("m", "o"): 0x3082, - ("y", "A"): 0x3083, - ("y", "a"): 0x3084, - ("y", "U"): 0x3085, - ("y", "u"): 0x3086, - ("y", "O"): 0x3087, - ("y", "o"): 0x3088, - ("r", "a"): 0x3089, - ("r", "i"): 0x308A, - ("r", "u"): 0x308B, - ("r", "e"): 0x308C, - ("r", "o"): 0x308D, - ("w", "A"): 0x308E, - ("w", "a"): 0x308F, - ("w", "i"): 0x3090, - ("w", "e"): 0x3091, - ("w", "o"): 0x3092, - ("n", "5"): 0x3093, - ("v", "u"): 0x3094, - ('"', "5"): 0x309B, - ("0", "5"): 0x309C, - ("*", "5"): 0x309D, - ("+", "5"): 0x309E, - ("a", "6"): 0x30A1, - ("A", "6"): 0x30A2, - ("i", "6"): 0x30A3, - ("I", "6"): 0x30A4, - ("u", "6"): 0x30A5, - ("U", "6"): 0x30A6, - ("e", "6"): 0x30A7, - ("E", "6"): 0x30A8, - ("o", "6"): 0x30A9, - ("O", "6"): 0x30AA, - ("K", "a"): 0x30AB, - ("G", "a"): 0x30AC, - ("K", "i"): 0x30AD, - ("G", "i"): 0x30AE, - ("K", "u"): 0x30AF, - ("G", "u"): 0x30B0, - ("K", "e"): 0x30B1, - ("G", "e"): 0x30B2, - ("K", "o"): 0x30B3, - ("G", "o"): 0x30B4, - ("S", "a"): 0x30B5, - ("Z", "a"): 0x30B6, - ("S", "i"): 0x30B7, - ("Z", "i"): 0x30B8, - ("S", "u"): 0x30B9, - ("Z", "u"): 0x30BA, - ("S", "e"): 0x30BB, - ("Z", "e"): 0x30BC, - ("S", "o"): 0x30BD, - ("Z", "o"): 0x30BE, - ("T", "a"): 0x30BF, - ("D", "a"): 0x30C0, - ("T", "i"): 0x30C1, - ("D", "i"): 0x30C2, - ("T", "U"): 0x30C3, - ("T", "u"): 0x30C4, - ("D", "u"): 0x30C5, - ("T", "e"): 0x30C6, - ("D", "e"): 0x30C7, - ("T", "o"): 0x30C8, - ("D", "o"): 0x30C9, - ("N", "a"): 0x30CA, - ("N", "i"): 0x30CB, - ("N", "u"): 0x30CC, - ("N", "e"): 0x30CD, - ("N", "o"): 0x30CE, - ("H", "a"): 0x30CF, - ("B", "a"): 0x30D0, - ("P", "a"): 0x30D1, - ("H", "i"): 0x30D2, - ("B", "i"): 0x30D3, - ("P", "i"): 0x30D4, - ("H", "u"): 0x30D5, - ("B", "u"): 0x30D6, - ("P", "u"): 0x30D7, - ("H", "e"): 0x30D8, - ("B", "e"): 0x30D9, - ("P", "e"): 0x30DA, - ("H", "o"): 0x30DB, - ("B", "o"): 0x30DC, - ("P", "o"): 0x30DD, - ("M", "a"): 0x30DE, - ("M", "i"): 0x30DF, - ("M", "u"): 0x30E0, - ("M", "e"): 0x30E1, - ("M", "o"): 0x30E2, - ("Y", "A"): 0x30E3, - ("Y", "a"): 0x30E4, - ("Y", "U"): 0x30E5, - ("Y", "u"): 0x30E6, - ("Y", "O"): 0x30E7, - ("Y", "o"): 0x30E8, - ("R", "a"): 0x30E9, - ("R", "i"): 0x30EA, - ("R", "u"): 0x30EB, - ("R", "e"): 0x30EC, - ("R", "o"): 0x30ED, - ("W", "A"): 0x30EE, - ("W", "a"): 0x30EF, - ("W", "i"): 0x30F0, - ("W", "e"): 0x30F1, - ("W", "o"): 0x30F2, - ("N", "6"): 0x30F3, - ("V", "u"): 0x30F4, - ("K", "A"): 0x30F5, - ("K", "E"): 0x30F6, - ("V", "a"): 0x30F7, - ("V", "i"): 0x30F8, - ("V", "e"): 0x30F9, - ("V", "o"): 0x30FA, - (".", "6"): 0x30FB, - ("-", "6"): 0x30FC, - ("*", "6"): 0x30FD, - ("+", "6"): 0x30FE, - ("b", "4"): 0x3105, - ("p", "4"): 0x3106, - ("m", "4"): 0x3107, - ("f", "4"): 0x3108, - ("d", "4"): 0x3109, - ("t", "4"): 0x310A, - ("n", "4"): 0x310B, - ("l", "4"): 0x310C, - ("g", "4"): 0x310D, - ("k", "4"): 0x310E, - ("h", "4"): 0x310F, - ("j", "4"): 0x3110, - ("q", "4"): 0x3111, - ("x", "4"): 0x3112, - ("z", "h"): 0x3113, - ("c", "h"): 0x3114, - ("s", "h"): 0x3115, - ("r", "4"): 0x3116, - ("z", "4"): 0x3117, - ("c", "4"): 0x3118, - ("s", "4"): 0x3119, - ("a", "4"): 0x311A, - ("o", "4"): 0x311B, - ("e", "4"): 0x311C, - ("a", "i"): 0x311E, - ("e", "i"): 0x311F, - ("a", "u"): 0x3120, - ("o", "u"): 0x3121, - ("a", "n"): 0x3122, - ("e", "n"): 0x3123, - ("a", "N"): 0x3124, - ("e", "N"): 0x3125, - ("e", "r"): 0x3126, - ("i", "4"): 0x3127, - ("u", "4"): 0x3128, - ("i", "u"): 0x3129, - ("v", "4"): 0x312A, - ("n", "G"): 0x312B, - ("g", "n"): 0x312C, - ("1", "c"): 0x3220, - ("2", "c"): 0x3221, - ("3", "c"): 0x3222, - ("4", "c"): 0x3223, - ("5", "c"): 0x3224, - ("6", "c"): 0x3225, - ("7", "c"): 0x3226, - ("8", "c"): 0x3227, - ("9", "c"): 0x3228, - # code points 0xe000 - 0xefff excluded, they have no assigned - # characters, only used in proposals. - ("f", "f"): 0xFB00, - ("f", "i"): 0xFB01, - ("f", "l"): 0xFB02, - ("f", "t"): 0xFB05, - ("s", "t"): 0xFB06, - # Vim 5.x compatible digraphs that don't conflict with the above - ("~", "!"): 161, - ("c", "|"): 162, - ("$", "$"): 163, - ("o", "x"): 164, # currency symbol in ISO 8859-1 - ("Y", "-"): 165, - ("|", "|"): 166, - ("c", "O"): 169, - ("-", ","): 172, - ("-", "="): 175, - ("~", "o"): 176, - ("2", "2"): 178, - ("3", "3"): 179, - ("p", "p"): 182, - ("~", "."): 183, - ("1", "1"): 185, - ("~", "?"): 191, - ("A", "`"): 192, - ("A", "^"): 194, - ("A", "~"): 195, - ("A", '"'): 196, - ("A", "@"): 197, - ("E", "`"): 200, - ("E", "^"): 202, - ("E", '"'): 203, - ("I", "`"): 204, - ("I", "^"): 206, - ("I", '"'): 207, - ("N", "~"): 209, - ("O", "`"): 210, - ("O", "^"): 212, - ("O", "~"): 213, - ("/", "\\"): 215, # multiplication symbol in ISO 8859-1 - ("U", "`"): 217, - ("U", "^"): 219, - ("I", "p"): 222, - ("a", "`"): 224, - ("a", "^"): 226, - ("a", "~"): 227, - ("a", '"'): 228, - ("a", "@"): 229, - ("e", "`"): 232, - ("e", "^"): 234, - ("e", '"'): 235, - ("i", "`"): 236, - ("i", "^"): 238, - ("n", "~"): 241, - ("o", "`"): 242, - ("o", "^"): 244, - ("o", "~"): 245, - ("u", "`"): 249, - ("u", "^"): 251, - ("y", '"'): 255, -} +# encoding: utf-8 +""" +Vi Digraphs. +This is a list of special characters that can be inserted in Vi insert mode by +pressing Control-K followed by to normal characters. + +Taken from Neovim and translated to Python: +https://raw.githubusercontent.com/neovim/neovim/master/src/nvim/digraph.c +""" +from typing import Dict, Tuple + +__all__ = [ + "DIGRAPHS", +] + +# digraphs for Unicode from RFC1345 +# (also work for ISO-8859-1 aka latin1) +DIGRAPHS: Dict[Tuple[str, str], int] = { + ("N", "U"): 0x00, + ("S", "H"): 0x01, + ("S", "X"): 0x02, + ("E", "X"): 0x03, + ("E", "T"): 0x04, + ("E", "Q"): 0x05, + ("A", "K"): 0x06, + ("B", "L"): 0x07, + ("B", "S"): 0x08, + ("H", "T"): 0x09, + ("L", "F"): 0x0A, + ("V", "T"): 0x0B, + ("F", "F"): 0x0C, + ("C", "R"): 0x0D, + ("S", "O"): 0x0E, + ("S", "I"): 0x0F, + ("D", "L"): 0x10, + ("D", "1"): 0x11, + ("D", "2"): 0x12, + ("D", "3"): 0x13, + ("D", "4"): 0x14, + ("N", "K"): 0x15, + ("S", "Y"): 0x16, + ("E", "B"): 0x17, + ("C", "N"): 0x18, + ("E", "M"): 0x19, + ("S", "B"): 0x1A, + ("E", "C"): 0x1B, + ("F", "S"): 0x1C, + ("G", "S"): 0x1D, + ("R", "S"): 0x1E, + ("U", "S"): 0x1F, + ("S", "P"): 0x20, + ("N", "b"): 0x23, + ("D", "O"): 0x24, + ("A", "t"): 0x40, + ("<", "("): 0x5B, + ("/", "/"): 0x5C, + (")", ">"): 0x5D, + ("'", ">"): 0x5E, + ("'", "!"): 0x60, + ("(", "!"): 0x7B, + ("!", "!"): 0x7C, + ("!", ")"): 0x7D, + ("'", "?"): 0x7E, + ("D", "T"): 0x7F, + ("P", "A"): 0x80, + ("H", "O"): 0x81, + ("B", "H"): 0x82, + ("N", "H"): 0x83, + ("I", "N"): 0x84, + ("N", "L"): 0x85, + ("S", "A"): 0x86, + ("E", "S"): 0x87, + ("H", "S"): 0x88, + ("H", "J"): 0x89, + ("V", "S"): 0x8A, + ("P", "D"): 0x8B, + ("P", "U"): 0x8C, + ("R", "I"): 0x8D, + ("S", "2"): 0x8E, + ("S", "3"): 0x8F, + ("D", "C"): 0x90, + ("P", "1"): 0x91, + ("P", "2"): 0x92, + ("T", "S"): 0x93, + ("C", "C"): 0x94, + ("M", "W"): 0x95, + ("S", "G"): 0x96, + ("E", "G"): 0x97, + ("S", "S"): 0x98, + ("G", "C"): 0x99, + ("S", "C"): 0x9A, + ("C", "I"): 0x9B, + ("S", "T"): 0x9C, + ("O", "C"): 0x9D, + ("P", "M"): 0x9E, + ("A", "C"): 0x9F, + ("N", "S"): 0xA0, + ("!", "I"): 0xA1, + ("C", "t"): 0xA2, + ("P", "d"): 0xA3, + ("C", "u"): 0xA4, + ("Y", "e"): 0xA5, + ("B", "B"): 0xA6, + ("S", "E"): 0xA7, + ("'", ":"): 0xA8, + ("C", "o"): 0xA9, + ("-", "a"): 0xAA, + ("<", "<"): 0xAB, + ("N", "O"): 0xAC, + ("-", "-"): 0xAD, + ("R", "g"): 0xAE, + ("'", "m"): 0xAF, + ("D", "G"): 0xB0, + ("+", "-"): 0xB1, + ("2", "S"): 0xB2, + ("3", "S"): 0xB3, + ("'", "'"): 0xB4, + ("M", "y"): 0xB5, + ("P", "I"): 0xB6, + (".", "M"): 0xB7, + ("'", ","): 0xB8, + ("1", "S"): 0xB9, + ("-", "o"): 0xBA, + (">", ">"): 0xBB, + ("1", "4"): 0xBC, + ("1", "2"): 0xBD, + ("3", "4"): 0xBE, + ("?", "I"): 0xBF, + ("A", "!"): 0xC0, + ("A", "'"): 0xC1, + ("A", ">"): 0xC2, + ("A", "?"): 0xC3, + ("A", ":"): 0xC4, + ("A", "A"): 0xC5, + ("A", "E"): 0xC6, + ("C", ","): 0xC7, + ("E", "!"): 0xC8, + ("E", "'"): 0xC9, + ("E", ">"): 0xCA, + ("E", ":"): 0xCB, + ("I", "!"): 0xCC, + ("I", "'"): 0xCD, + ("I", ">"): 0xCE, + ("I", ":"): 0xCF, + ("D", "-"): 0xD0, + ("N", "?"): 0xD1, + ("O", "!"): 0xD2, + ("O", "'"): 0xD3, + ("O", ">"): 0xD4, + ("O", "?"): 0xD5, + ("O", ":"): 0xD6, + ("*", "X"): 0xD7, + ("O", "/"): 0xD8, + ("U", "!"): 0xD9, + ("U", "'"): 0xDA, + ("U", ">"): 0xDB, + ("U", ":"): 0xDC, + ("Y", "'"): 0xDD, + ("T", "H"): 0xDE, + ("s", "s"): 0xDF, + ("a", "!"): 0xE0, + ("a", "'"): 0xE1, + ("a", ">"): 0xE2, + ("a", "?"): 0xE3, + ("a", ":"): 0xE4, + ("a", "a"): 0xE5, + ("a", "e"): 0xE6, + ("c", ","): 0xE7, + ("e", "!"): 0xE8, + ("e", "'"): 0xE9, + ("e", ">"): 0xEA, + ("e", ":"): 0xEB, + ("i", "!"): 0xEC, + ("i", "'"): 0xED, + ("i", ">"): 0xEE, + ("i", ":"): 0xEF, + ("d", "-"): 0xF0, + ("n", "?"): 0xF1, + ("o", "!"): 0xF2, + ("o", "'"): 0xF3, + ("o", ">"): 0xF4, + ("o", "?"): 0xF5, + ("o", ":"): 0xF6, + ("-", ":"): 0xF7, + ("o", "/"): 0xF8, + ("u", "!"): 0xF9, + ("u", "'"): 0xFA, + ("u", ">"): 0xFB, + ("u", ":"): 0xFC, + ("y", "'"): 0xFD, + ("t", "h"): 0xFE, + ("y", ":"): 0xFF, + ("A", "-"): 0x0100, + ("a", "-"): 0x0101, + ("A", "("): 0x0102, + ("a", "("): 0x0103, + ("A", ";"): 0x0104, + ("a", ";"): 0x0105, + ("C", "'"): 0x0106, + ("c", "'"): 0x0107, + ("C", ">"): 0x0108, + ("c", ">"): 0x0109, + ("C", "."): 0x010A, + ("c", "."): 0x010B, + ("C", "<"): 0x010C, + ("c", "<"): 0x010D, + ("D", "<"): 0x010E, + ("d", "<"): 0x010F, + ("D", "/"): 0x0110, + ("d", "/"): 0x0111, + ("E", "-"): 0x0112, + ("e", "-"): 0x0113, + ("E", "("): 0x0114, + ("e", "("): 0x0115, + ("E", "."): 0x0116, + ("e", "."): 0x0117, + ("E", ";"): 0x0118, + ("e", ";"): 0x0119, + ("E", "<"): 0x011A, + ("e", "<"): 0x011B, + ("G", ">"): 0x011C, + ("g", ">"): 0x011D, + ("G", "("): 0x011E, + ("g", "("): 0x011F, + ("G", "."): 0x0120, + ("g", "."): 0x0121, + ("G", ","): 0x0122, + ("g", ","): 0x0123, + ("H", ">"): 0x0124, + ("h", ">"): 0x0125, + ("H", "/"): 0x0126, + ("h", "/"): 0x0127, + ("I", "?"): 0x0128, + ("i", "?"): 0x0129, + ("I", "-"): 0x012A, + ("i", "-"): 0x012B, + ("I", "("): 0x012C, + ("i", "("): 0x012D, + ("I", ";"): 0x012E, + ("i", ";"): 0x012F, + ("I", "."): 0x0130, + ("i", "."): 0x0131, + ("I", "J"): 0x0132, + ("i", "j"): 0x0133, + ("J", ">"): 0x0134, + ("j", ">"): 0x0135, + ("K", ","): 0x0136, + ("k", ","): 0x0137, + ("k", "k"): 0x0138, + ("L", "'"): 0x0139, + ("l", "'"): 0x013A, + ("L", ","): 0x013B, + ("l", ","): 0x013C, + ("L", "<"): 0x013D, + ("l", "<"): 0x013E, + ("L", "."): 0x013F, + ("l", "."): 0x0140, + ("L", "/"): 0x0141, + ("l", "/"): 0x0142, + ("N", "'"): 0x0143, + ("n", "'"): 0x0144, + ("N", ","): 0x0145, + ("n", ","): 0x0146, + ("N", "<"): 0x0147, + ("n", "<"): 0x0148, + ("'", "n"): 0x0149, + ("N", "G"): 0x014A, + ("n", "g"): 0x014B, + ("O", "-"): 0x014C, + ("o", "-"): 0x014D, + ("O", "("): 0x014E, + ("o", "("): 0x014F, + ("O", '"'): 0x0150, + ("o", '"'): 0x0151, + ("O", "E"): 0x0152, + ("o", "e"): 0x0153, + ("R", "'"): 0x0154, + ("r", "'"): 0x0155, + ("R", ","): 0x0156, + ("r", ","): 0x0157, + ("R", "<"): 0x0158, + ("r", "<"): 0x0159, + ("S", "'"): 0x015A, + ("s", "'"): 0x015B, + ("S", ">"): 0x015C, + ("s", ">"): 0x015D, + ("S", ","): 0x015E, + ("s", ","): 0x015F, + ("S", "<"): 0x0160, + ("s", "<"): 0x0161, + ("T", ","): 0x0162, + ("t", ","): 0x0163, + ("T", "<"): 0x0164, + ("t", "<"): 0x0165, + ("T", "/"): 0x0166, + ("t", "/"): 0x0167, + ("U", "?"): 0x0168, + ("u", "?"): 0x0169, + ("U", "-"): 0x016A, + ("u", "-"): 0x016B, + ("U", "("): 0x016C, + ("u", "("): 0x016D, + ("U", "0"): 0x016E, + ("u", "0"): 0x016F, + ("U", '"'): 0x0170, + ("u", '"'): 0x0171, + ("U", ";"): 0x0172, + ("u", ";"): 0x0173, + ("W", ">"): 0x0174, + ("w", ">"): 0x0175, + ("Y", ">"): 0x0176, + ("y", ">"): 0x0177, + ("Y", ":"): 0x0178, + ("Z", "'"): 0x0179, + ("z", "'"): 0x017A, + ("Z", "."): 0x017B, + ("z", "."): 0x017C, + ("Z", "<"): 0x017D, + ("z", "<"): 0x017E, + ("O", "9"): 0x01A0, + ("o", "9"): 0x01A1, + ("O", "I"): 0x01A2, + ("o", "i"): 0x01A3, + ("y", "r"): 0x01A6, + ("U", "9"): 0x01AF, + ("u", "9"): 0x01B0, + ("Z", "/"): 0x01B5, + ("z", "/"): 0x01B6, + ("E", "D"): 0x01B7, + ("A", "<"): 0x01CD, + ("a", "<"): 0x01CE, + ("I", "<"): 0x01CF, + ("i", "<"): 0x01D0, + ("O", "<"): 0x01D1, + ("o", "<"): 0x01D2, + ("U", "<"): 0x01D3, + ("u", "<"): 0x01D4, + ("A", "1"): 0x01DE, + ("a", "1"): 0x01DF, + ("A", "7"): 0x01E0, + ("a", "7"): 0x01E1, + ("A", "3"): 0x01E2, + ("a", "3"): 0x01E3, + ("G", "/"): 0x01E4, + ("g", "/"): 0x01E5, + ("G", "<"): 0x01E6, + ("g", "<"): 0x01E7, + ("K", "<"): 0x01E8, + ("k", "<"): 0x01E9, + ("O", ";"): 0x01EA, + ("o", ";"): 0x01EB, + ("O", "1"): 0x01EC, + ("o", "1"): 0x01ED, + ("E", "Z"): 0x01EE, + ("e", "z"): 0x01EF, + ("j", "<"): 0x01F0, + ("G", "'"): 0x01F4, + ("g", "'"): 0x01F5, + (";", "S"): 0x02BF, + ("'", "<"): 0x02C7, + ("'", "("): 0x02D8, + ("'", "."): 0x02D9, + ("'", "0"): 0x02DA, + ("'", ";"): 0x02DB, + ("'", '"'): 0x02DD, + ("A", "%"): 0x0386, + ("E", "%"): 0x0388, + ("Y", "%"): 0x0389, + ("I", "%"): 0x038A, + ("O", "%"): 0x038C, + ("U", "%"): 0x038E, + ("W", "%"): 0x038F, + ("i", "3"): 0x0390, + ("A", "*"): 0x0391, + ("B", "*"): 0x0392, + ("G", "*"): 0x0393, + ("D", "*"): 0x0394, + ("E", "*"): 0x0395, + ("Z", "*"): 0x0396, + ("Y", "*"): 0x0397, + ("H", "*"): 0x0398, + ("I", "*"): 0x0399, + ("K", "*"): 0x039A, + ("L", "*"): 0x039B, + ("M", "*"): 0x039C, + ("N", "*"): 0x039D, + ("C", "*"): 0x039E, + ("O", "*"): 0x039F, + ("P", "*"): 0x03A0, + ("R", "*"): 0x03A1, + ("S", "*"): 0x03A3, + ("T", "*"): 0x03A4, + ("U", "*"): 0x03A5, + ("F", "*"): 0x03A6, + ("X", "*"): 0x03A7, + ("Q", "*"): 0x03A8, + ("W", "*"): 0x03A9, + ("J", "*"): 0x03AA, + ("V", "*"): 0x03AB, + ("a", "%"): 0x03AC, + ("e", "%"): 0x03AD, + ("y", "%"): 0x03AE, + ("i", "%"): 0x03AF, + ("u", "3"): 0x03B0, + ("a", "*"): 0x03B1, + ("b", "*"): 0x03B2, + ("g", "*"): 0x03B3, + ("d", "*"): 0x03B4, + ("e", "*"): 0x03B5, + ("z", "*"): 0x03B6, + ("y", "*"): 0x03B7, + ("h", "*"): 0x03B8, + ("i", "*"): 0x03B9, + ("k", "*"): 0x03BA, + ("l", "*"): 0x03BB, + ("m", "*"): 0x03BC, + ("n", "*"): 0x03BD, + ("c", "*"): 0x03BE, + ("o", "*"): 0x03BF, + ("p", "*"): 0x03C0, + ("r", "*"): 0x03C1, + ("*", "s"): 0x03C2, + ("s", "*"): 0x03C3, + ("t", "*"): 0x03C4, + ("u", "*"): 0x03C5, + ("f", "*"): 0x03C6, + ("x", "*"): 0x03C7, + ("q", "*"): 0x03C8, + ("w", "*"): 0x03C9, + ("j", "*"): 0x03CA, + ("v", "*"): 0x03CB, + ("o", "%"): 0x03CC, + ("u", "%"): 0x03CD, + ("w", "%"): 0x03CE, + ("'", "G"): 0x03D8, + (",", "G"): 0x03D9, + ("T", "3"): 0x03DA, + ("t", "3"): 0x03DB, + ("M", "3"): 0x03DC, + ("m", "3"): 0x03DD, + ("K", "3"): 0x03DE, + ("k", "3"): 0x03DF, + ("P", "3"): 0x03E0, + ("p", "3"): 0x03E1, + ("'", "%"): 0x03F4, + ("j", "3"): 0x03F5, + ("I", "O"): 0x0401, + ("D", "%"): 0x0402, + ("G", "%"): 0x0403, + ("I", "E"): 0x0404, + ("D", "S"): 0x0405, + ("I", "I"): 0x0406, + ("Y", "I"): 0x0407, + ("J", "%"): 0x0408, + ("L", "J"): 0x0409, + ("N", "J"): 0x040A, + ("T", "s"): 0x040B, + ("K", "J"): 0x040C, + ("V", "%"): 0x040E, + ("D", "Z"): 0x040F, + ("A", "="): 0x0410, + ("B", "="): 0x0411, + ("V", "="): 0x0412, + ("G", "="): 0x0413, + ("D", "="): 0x0414, + ("E", "="): 0x0415, + ("Z", "%"): 0x0416, + ("Z", "="): 0x0417, + ("I", "="): 0x0418, + ("J", "="): 0x0419, + ("K", "="): 0x041A, + ("L", "="): 0x041B, + ("M", "="): 0x041C, + ("N", "="): 0x041D, + ("O", "="): 0x041E, + ("P", "="): 0x041F, + ("R", "="): 0x0420, + ("S", "="): 0x0421, + ("T", "="): 0x0422, + ("U", "="): 0x0423, + ("F", "="): 0x0424, + ("H", "="): 0x0425, + ("C", "="): 0x0426, + ("C", "%"): 0x0427, + ("S", "%"): 0x0428, + ("S", "c"): 0x0429, + ("=", '"'): 0x042A, + ("Y", "="): 0x042B, + ("%", '"'): 0x042C, + ("J", "E"): 0x042D, + ("J", "U"): 0x042E, + ("J", "A"): 0x042F, + ("a", "="): 0x0430, + ("b", "="): 0x0431, + ("v", "="): 0x0432, + ("g", "="): 0x0433, + ("d", "="): 0x0434, + ("e", "="): 0x0435, + ("z", "%"): 0x0436, + ("z", "="): 0x0437, + ("i", "="): 0x0438, + ("j", "="): 0x0439, + ("k", "="): 0x043A, + ("l", "="): 0x043B, + ("m", "="): 0x043C, + ("n", "="): 0x043D, + ("o", "="): 0x043E, + ("p", "="): 0x043F, + ("r", "="): 0x0440, + ("s", "="): 0x0441, + ("t", "="): 0x0442, + ("u", "="): 0x0443, + ("f", "="): 0x0444, + ("h", "="): 0x0445, + ("c", "="): 0x0446, + ("c", "%"): 0x0447, + ("s", "%"): 0x0448, + ("s", "c"): 0x0449, + ("=", "'"): 0x044A, + ("y", "="): 0x044B, + ("%", "'"): 0x044C, + ("j", "e"): 0x044D, + ("j", "u"): 0x044E, + ("j", "a"): 0x044F, + ("i", "o"): 0x0451, + ("d", "%"): 0x0452, + ("g", "%"): 0x0453, + ("i", "e"): 0x0454, + ("d", "s"): 0x0455, + ("i", "i"): 0x0456, + ("y", "i"): 0x0457, + ("j", "%"): 0x0458, + ("l", "j"): 0x0459, + ("n", "j"): 0x045A, + ("t", "s"): 0x045B, + ("k", "j"): 0x045C, + ("v", "%"): 0x045E, + ("d", "z"): 0x045F, + ("Y", "3"): 0x0462, + ("y", "3"): 0x0463, + ("O", "3"): 0x046A, + ("o", "3"): 0x046B, + ("F", "3"): 0x0472, + ("f", "3"): 0x0473, + ("V", "3"): 0x0474, + ("v", "3"): 0x0475, + ("C", "3"): 0x0480, + ("c", "3"): 0x0481, + ("G", "3"): 0x0490, + ("g", "3"): 0x0491, + ("A", "+"): 0x05D0, + ("B", "+"): 0x05D1, + ("G", "+"): 0x05D2, + ("D", "+"): 0x05D3, + ("H", "+"): 0x05D4, + ("W", "+"): 0x05D5, + ("Z", "+"): 0x05D6, + ("X", "+"): 0x05D7, + ("T", "j"): 0x05D8, + ("J", "+"): 0x05D9, + ("K", "%"): 0x05DA, + ("K", "+"): 0x05DB, + ("L", "+"): 0x05DC, + ("M", "%"): 0x05DD, + ("M", "+"): 0x05DE, + ("N", "%"): 0x05DF, + ("N", "+"): 0x05E0, + ("S", "+"): 0x05E1, + ("E", "+"): 0x05E2, + ("P", "%"): 0x05E3, + ("P", "+"): 0x05E4, + ("Z", "j"): 0x05E5, + ("Z", "J"): 0x05E6, + ("Q", "+"): 0x05E7, + ("R", "+"): 0x05E8, + ("S", "h"): 0x05E9, + ("T", "+"): 0x05EA, + (",", "+"): 0x060C, + (";", "+"): 0x061B, + ("?", "+"): 0x061F, + ("H", "'"): 0x0621, + ("a", "M"): 0x0622, + ("a", "H"): 0x0623, + ("w", "H"): 0x0624, + ("a", "h"): 0x0625, + ("y", "H"): 0x0626, + ("a", "+"): 0x0627, + ("b", "+"): 0x0628, + ("t", "m"): 0x0629, + ("t", "+"): 0x062A, + ("t", "k"): 0x062B, + ("g", "+"): 0x062C, + ("h", "k"): 0x062D, + ("x", "+"): 0x062E, + ("d", "+"): 0x062F, + ("d", "k"): 0x0630, + ("r", "+"): 0x0631, + ("z", "+"): 0x0632, + ("s", "+"): 0x0633, + ("s", "n"): 0x0634, + ("c", "+"): 0x0635, + ("d", "d"): 0x0636, + ("t", "j"): 0x0637, + ("z", "H"): 0x0638, + ("e", "+"): 0x0639, + ("i", "+"): 0x063A, + ("+", "+"): 0x0640, + ("f", "+"): 0x0641, + ("q", "+"): 0x0642, + ("k", "+"): 0x0643, + ("l", "+"): 0x0644, + ("m", "+"): 0x0645, + ("n", "+"): 0x0646, + ("h", "+"): 0x0647, + ("w", "+"): 0x0648, + ("j", "+"): 0x0649, + ("y", "+"): 0x064A, + (":", "+"): 0x064B, + ('"', "+"): 0x064C, + ("=", "+"): 0x064D, + ("/", "+"): 0x064E, + ("'", "+"): 0x064F, + ("1", "+"): 0x0650, + ("3", "+"): 0x0651, + ("0", "+"): 0x0652, + ("a", "S"): 0x0670, + ("p", "+"): 0x067E, + ("v", "+"): 0x06A4, + ("g", "f"): 0x06AF, + ("0", "a"): 0x06F0, + ("1", "a"): 0x06F1, + ("2", "a"): 0x06F2, + ("3", "a"): 0x06F3, + ("4", "a"): 0x06F4, + ("5", "a"): 0x06F5, + ("6", "a"): 0x06F6, + ("7", "a"): 0x06F7, + ("8", "a"): 0x06F8, + ("9", "a"): 0x06F9, + ("B", "."): 0x1E02, + ("b", "."): 0x1E03, + ("B", "_"): 0x1E06, + ("b", "_"): 0x1E07, + ("D", "."): 0x1E0A, + ("d", "."): 0x1E0B, + ("D", "_"): 0x1E0E, + ("d", "_"): 0x1E0F, + ("D", ","): 0x1E10, + ("d", ","): 0x1E11, + ("F", "."): 0x1E1E, + ("f", "."): 0x1E1F, + ("G", "-"): 0x1E20, + ("g", "-"): 0x1E21, + ("H", "."): 0x1E22, + ("h", "."): 0x1E23, + ("H", ":"): 0x1E26, + ("h", ":"): 0x1E27, + ("H", ","): 0x1E28, + ("h", ","): 0x1E29, + ("K", "'"): 0x1E30, + ("k", "'"): 0x1E31, + ("K", "_"): 0x1E34, + ("k", "_"): 0x1E35, + ("L", "_"): 0x1E3A, + ("l", "_"): 0x1E3B, + ("M", "'"): 0x1E3E, + ("m", "'"): 0x1E3F, + ("M", "."): 0x1E40, + ("m", "."): 0x1E41, + ("N", "."): 0x1E44, + ("n", "."): 0x1E45, + ("N", "_"): 0x1E48, + ("n", "_"): 0x1E49, + ("P", "'"): 0x1E54, + ("p", "'"): 0x1E55, + ("P", "."): 0x1E56, + ("p", "."): 0x1E57, + ("R", "."): 0x1E58, + ("r", "."): 0x1E59, + ("R", "_"): 0x1E5E, + ("r", "_"): 0x1E5F, + ("S", "."): 0x1E60, + ("s", "."): 0x1E61, + ("T", "."): 0x1E6A, + ("t", "."): 0x1E6B, + ("T", "_"): 0x1E6E, + ("t", "_"): 0x1E6F, + ("V", "?"): 0x1E7C, + ("v", "?"): 0x1E7D, + ("W", "!"): 0x1E80, + ("w", "!"): 0x1E81, + ("W", "'"): 0x1E82, + ("w", "'"): 0x1E83, + ("W", ":"): 0x1E84, + ("w", ":"): 0x1E85, + ("W", "."): 0x1E86, + ("w", "."): 0x1E87, + ("X", "."): 0x1E8A, + ("x", "."): 0x1E8B, + ("X", ":"): 0x1E8C, + ("x", ":"): 0x1E8D, + ("Y", "."): 0x1E8E, + ("y", "."): 0x1E8F, + ("Z", ">"): 0x1E90, + ("z", ">"): 0x1E91, + ("Z", "_"): 0x1E94, + ("z", "_"): 0x1E95, + ("h", "_"): 0x1E96, + ("t", ":"): 0x1E97, + ("w", "0"): 0x1E98, + ("y", "0"): 0x1E99, + ("A", "2"): 0x1EA2, + ("a", "2"): 0x1EA3, + ("E", "2"): 0x1EBA, + ("e", "2"): 0x1EBB, + ("E", "?"): 0x1EBC, + ("e", "?"): 0x1EBD, + ("I", "2"): 0x1EC8, + ("i", "2"): 0x1EC9, + ("O", "2"): 0x1ECE, + ("o", "2"): 0x1ECF, + ("U", "2"): 0x1EE6, + ("u", "2"): 0x1EE7, + ("Y", "!"): 0x1EF2, + ("y", "!"): 0x1EF3, + ("Y", "2"): 0x1EF6, + ("y", "2"): 0x1EF7, + ("Y", "?"): 0x1EF8, + ("y", "?"): 0x1EF9, + (";", "'"): 0x1F00, + (",", "'"): 0x1F01, + (";", "!"): 0x1F02, + (",", "!"): 0x1F03, + ("?", ";"): 0x1F04, + ("?", ","): 0x1F05, + ("!", ":"): 0x1F06, + ("?", ":"): 0x1F07, + ("1", "N"): 0x2002, + ("1", "M"): 0x2003, + ("3", "M"): 0x2004, + ("4", "M"): 0x2005, + ("6", "M"): 0x2006, + ("1", "T"): 0x2009, + ("1", "H"): 0x200A, + ("-", "1"): 0x2010, + ("-", "N"): 0x2013, + ("-", "M"): 0x2014, + ("-", "3"): 0x2015, + ("!", "2"): 0x2016, + ("=", "2"): 0x2017, + ("'", "6"): 0x2018, + ("'", "9"): 0x2019, + (".", "9"): 0x201A, + ("9", "'"): 0x201B, + ('"', "6"): 0x201C, + ('"', "9"): 0x201D, + (":", "9"): 0x201E, + ("9", '"'): 0x201F, + ("/", "-"): 0x2020, + ("/", "="): 0x2021, + (".", "."): 0x2025, + ("%", "0"): 0x2030, + ("1", "'"): 0x2032, + ("2", "'"): 0x2033, + ("3", "'"): 0x2034, + ("1", '"'): 0x2035, + ("2", '"'): 0x2036, + ("3", '"'): 0x2037, + ("C", "a"): 0x2038, + ("<", "1"): 0x2039, + (">", "1"): 0x203A, + (":", "X"): 0x203B, + ("'", "-"): 0x203E, + ("/", "f"): 0x2044, + ("0", "S"): 0x2070, + ("4", "S"): 0x2074, + ("5", "S"): 0x2075, + ("6", "S"): 0x2076, + ("7", "S"): 0x2077, + ("8", "S"): 0x2078, + ("9", "S"): 0x2079, + ("+", "S"): 0x207A, + ("-", "S"): 0x207B, + ("=", "S"): 0x207C, + ("(", "S"): 0x207D, + (")", "S"): 0x207E, + ("n", "S"): 0x207F, + ("0", "s"): 0x2080, + ("1", "s"): 0x2081, + ("2", "s"): 0x2082, + ("3", "s"): 0x2083, + ("4", "s"): 0x2084, + ("5", "s"): 0x2085, + ("6", "s"): 0x2086, + ("7", "s"): 0x2087, + ("8", "s"): 0x2088, + ("9", "s"): 0x2089, + ("+", "s"): 0x208A, + ("-", "s"): 0x208B, + ("=", "s"): 0x208C, + ("(", "s"): 0x208D, + (")", "s"): 0x208E, + ("L", "i"): 0x20A4, + ("P", "t"): 0x20A7, + ("W", "="): 0x20A9, + ("=", "e"): 0x20AC, # euro + ("E", "u"): 0x20AC, # euro + ("=", "R"): 0x20BD, # rouble + ("=", "P"): 0x20BD, # rouble + ("o", "C"): 0x2103, + ("c", "o"): 0x2105, + ("o", "F"): 0x2109, + ("N", "0"): 0x2116, + ("P", "O"): 0x2117, + ("R", "x"): 0x211E, + ("S", "M"): 0x2120, + ("T", "M"): 0x2122, + ("O", "m"): 0x2126, + ("A", "O"): 0x212B, + ("1", "3"): 0x2153, + ("2", "3"): 0x2154, + ("1", "5"): 0x2155, + ("2", "5"): 0x2156, + ("3", "5"): 0x2157, + ("4", "5"): 0x2158, + ("1", "6"): 0x2159, + ("5", "6"): 0x215A, + ("1", "8"): 0x215B, + ("3", "8"): 0x215C, + ("5", "8"): 0x215D, + ("7", "8"): 0x215E, + ("1", "R"): 0x2160, + ("2", "R"): 0x2161, + ("3", "R"): 0x2162, + ("4", "R"): 0x2163, + ("5", "R"): 0x2164, + ("6", "R"): 0x2165, + ("7", "R"): 0x2166, + ("8", "R"): 0x2167, + ("9", "R"): 0x2168, + ("a", "R"): 0x2169, + ("b", "R"): 0x216A, + ("c", "R"): 0x216B, + ("1", "r"): 0x2170, + ("2", "r"): 0x2171, + ("3", "r"): 0x2172, + ("4", "r"): 0x2173, + ("5", "r"): 0x2174, + ("6", "r"): 0x2175, + ("7", "r"): 0x2176, + ("8", "r"): 0x2177, + ("9", "r"): 0x2178, + ("a", "r"): 0x2179, + ("b", "r"): 0x217A, + ("c", "r"): 0x217B, + ("<", "-"): 0x2190, + ("-", "!"): 0x2191, + ("-", ">"): 0x2192, + ("-", "v"): 0x2193, + ("<", ">"): 0x2194, + ("U", "D"): 0x2195, + ("<", "="): 0x21D0, + ("=", ">"): 0x21D2, + ("=", "="): 0x21D4, + ("F", "A"): 0x2200, + ("d", "P"): 0x2202, + ("T", "E"): 0x2203, + ("/", "0"): 0x2205, + ("D", "E"): 0x2206, + ("N", "B"): 0x2207, + ("(", "-"): 0x2208, + ("-", ")"): 0x220B, + ("*", "P"): 0x220F, + ("+", "Z"): 0x2211, + ("-", "2"): 0x2212, + ("-", "+"): 0x2213, + ("*", "-"): 0x2217, + ("O", "b"): 0x2218, + ("S", "b"): 0x2219, + ("R", "T"): 0x221A, + ("0", "("): 0x221D, + ("0", "0"): 0x221E, + ("-", "L"): 0x221F, + ("-", "V"): 0x2220, + ("P", "P"): 0x2225, + ("A", "N"): 0x2227, + ("O", "R"): 0x2228, + ("(", "U"): 0x2229, + (")", "U"): 0x222A, + ("I", "n"): 0x222B, + ("D", "I"): 0x222C, + ("I", "o"): 0x222E, + (".", ":"): 0x2234, + (":", "."): 0x2235, + (":", "R"): 0x2236, + (":", ":"): 0x2237, + ("?", "1"): 0x223C, + ("C", "G"): 0x223E, + ("?", "-"): 0x2243, + ("?", "="): 0x2245, + ("?", "2"): 0x2248, + ("=", "?"): 0x224C, + ("H", "I"): 0x2253, + ("!", "="): 0x2260, + ("=", "3"): 0x2261, + ("=", "<"): 0x2264, + (">", "="): 0x2265, + ("<", "*"): 0x226A, + ("*", ">"): 0x226B, + ("!", "<"): 0x226E, + ("!", ">"): 0x226F, + ("(", "C"): 0x2282, + (")", "C"): 0x2283, + ("(", "_"): 0x2286, + (")", "_"): 0x2287, + ("0", "."): 0x2299, + ("0", "2"): 0x229A, + ("-", "T"): 0x22A5, + (".", "P"): 0x22C5, + (":", "3"): 0x22EE, + (".", "3"): 0x22EF, + ("E", "h"): 0x2302, + ("<", "7"): 0x2308, + (">", "7"): 0x2309, + ("7", "<"): 0x230A, + ("7", ">"): 0x230B, + ("N", "I"): 0x2310, + ("(", "A"): 0x2312, + ("T", "R"): 0x2315, + ("I", "u"): 0x2320, + ("I", "l"): 0x2321, + ("<", "/"): 0x2329, + ("/", ">"): 0x232A, + ("V", "s"): 0x2423, + ("1", "h"): 0x2440, + ("3", "h"): 0x2441, + ("2", "h"): 0x2442, + ("4", "h"): 0x2443, + ("1", "j"): 0x2446, + ("2", "j"): 0x2447, + ("3", "j"): 0x2448, + ("4", "j"): 0x2449, + ("1", "."): 0x2488, + ("2", "."): 0x2489, + ("3", "."): 0x248A, + ("4", "."): 0x248B, + ("5", "."): 0x248C, + ("6", "."): 0x248D, + ("7", "."): 0x248E, + ("8", "."): 0x248F, + ("9", "."): 0x2490, + ("h", "h"): 0x2500, + ("H", "H"): 0x2501, + ("v", "v"): 0x2502, + ("V", "V"): 0x2503, + ("3", "-"): 0x2504, + ("3", "_"): 0x2505, + ("3", "!"): 0x2506, + ("3", "/"): 0x2507, + ("4", "-"): 0x2508, + ("4", "_"): 0x2509, + ("4", "!"): 0x250A, + ("4", "/"): 0x250B, + ("d", "r"): 0x250C, + ("d", "R"): 0x250D, + ("D", "r"): 0x250E, + ("D", "R"): 0x250F, + ("d", "l"): 0x2510, + ("d", "L"): 0x2511, + ("D", "l"): 0x2512, + ("L", "D"): 0x2513, + ("u", "r"): 0x2514, + ("u", "R"): 0x2515, + ("U", "r"): 0x2516, + ("U", "R"): 0x2517, + ("u", "l"): 0x2518, + ("u", "L"): 0x2519, + ("U", "l"): 0x251A, + ("U", "L"): 0x251B, + ("v", "r"): 0x251C, + ("v", "R"): 0x251D, + ("V", "r"): 0x2520, + ("V", "R"): 0x2523, + ("v", "l"): 0x2524, + ("v", "L"): 0x2525, + ("V", "l"): 0x2528, + ("V", "L"): 0x252B, + ("d", "h"): 0x252C, + ("d", "H"): 0x252F, + ("D", "h"): 0x2530, + ("D", "H"): 0x2533, + ("u", "h"): 0x2534, + ("u", "H"): 0x2537, + ("U", "h"): 0x2538, + ("U", "H"): 0x253B, + ("v", "h"): 0x253C, + ("v", "H"): 0x253F, + ("V", "h"): 0x2542, + ("V", "H"): 0x254B, + ("F", "D"): 0x2571, + ("B", "D"): 0x2572, + ("T", "B"): 0x2580, + ("L", "B"): 0x2584, + ("F", "B"): 0x2588, + ("l", "B"): 0x258C, + ("R", "B"): 0x2590, + (".", "S"): 0x2591, + (":", "S"): 0x2592, + ("?", "S"): 0x2593, + ("f", "S"): 0x25A0, + ("O", "S"): 0x25A1, + ("R", "O"): 0x25A2, + ("R", "r"): 0x25A3, + ("R", "F"): 0x25A4, + ("R", "Y"): 0x25A5, + ("R", "H"): 0x25A6, + ("R", "Z"): 0x25A7, + ("R", "K"): 0x25A8, + ("R", "X"): 0x25A9, + ("s", "B"): 0x25AA, + ("S", "R"): 0x25AC, + ("O", "r"): 0x25AD, + ("U", "T"): 0x25B2, + ("u", "T"): 0x25B3, + ("P", "R"): 0x25B6, + ("T", "r"): 0x25B7, + ("D", "t"): 0x25BC, + ("d", "T"): 0x25BD, + ("P", "L"): 0x25C0, + ("T", "l"): 0x25C1, + ("D", "b"): 0x25C6, + ("D", "w"): 0x25C7, + ("L", "Z"): 0x25CA, + ("0", "m"): 0x25CB, + ("0", "o"): 0x25CE, + ("0", "M"): 0x25CF, + ("0", "L"): 0x25D0, + ("0", "R"): 0x25D1, + ("S", "n"): 0x25D8, + ("I", "c"): 0x25D9, + ("F", "d"): 0x25E2, + ("B", "d"): 0x25E3, + ("*", "2"): 0x2605, + ("*", "1"): 0x2606, + ("<", "H"): 0x261C, + (">", "H"): 0x261E, + ("0", "u"): 0x263A, + ("0", "U"): 0x263B, + ("S", "U"): 0x263C, + ("F", "m"): 0x2640, + ("M", "l"): 0x2642, + ("c", "S"): 0x2660, + ("c", "H"): 0x2661, + ("c", "D"): 0x2662, + ("c", "C"): 0x2663, + ("M", "d"): 0x2669, + ("M", "8"): 0x266A, + ("M", "2"): 0x266B, + ("M", "b"): 0x266D, + ("M", "x"): 0x266E, + ("M", "X"): 0x266F, + ("O", "K"): 0x2713, + ("X", "X"): 0x2717, + ("-", "X"): 0x2720, + ("I", "S"): 0x3000, + (",", "_"): 0x3001, + (".", "_"): 0x3002, + ("+", '"'): 0x3003, + ("+", "_"): 0x3004, + ("*", "_"): 0x3005, + (";", "_"): 0x3006, + ("0", "_"): 0x3007, + ("<", "+"): 0x300A, + (">", "+"): 0x300B, + ("<", "'"): 0x300C, + (">", "'"): 0x300D, + ("<", '"'): 0x300E, + (">", '"'): 0x300F, + ("(", '"'): 0x3010, + (")", '"'): 0x3011, + ("=", "T"): 0x3012, + ("=", "_"): 0x3013, + ("(", "'"): 0x3014, + (")", "'"): 0x3015, + ("(", "I"): 0x3016, + (")", "I"): 0x3017, + ("-", "?"): 0x301C, + ("A", "5"): 0x3041, + ("a", "5"): 0x3042, + ("I", "5"): 0x3043, + ("i", "5"): 0x3044, + ("U", "5"): 0x3045, + ("u", "5"): 0x3046, + ("E", "5"): 0x3047, + ("e", "5"): 0x3048, + ("O", "5"): 0x3049, + ("o", "5"): 0x304A, + ("k", "a"): 0x304B, + ("g", "a"): 0x304C, + ("k", "i"): 0x304D, + ("g", "i"): 0x304E, + ("k", "u"): 0x304F, + ("g", "u"): 0x3050, + ("k", "e"): 0x3051, + ("g", "e"): 0x3052, + ("k", "o"): 0x3053, + ("g", "o"): 0x3054, + ("s", "a"): 0x3055, + ("z", "a"): 0x3056, + ("s", "i"): 0x3057, + ("z", "i"): 0x3058, + ("s", "u"): 0x3059, + ("z", "u"): 0x305A, + ("s", "e"): 0x305B, + ("z", "e"): 0x305C, + ("s", "o"): 0x305D, + ("z", "o"): 0x305E, + ("t", "a"): 0x305F, + ("d", "a"): 0x3060, + ("t", "i"): 0x3061, + ("d", "i"): 0x3062, + ("t", "U"): 0x3063, + ("t", "u"): 0x3064, + ("d", "u"): 0x3065, + ("t", "e"): 0x3066, + ("d", "e"): 0x3067, + ("t", "o"): 0x3068, + ("d", "o"): 0x3069, + ("n", "a"): 0x306A, + ("n", "i"): 0x306B, + ("n", "u"): 0x306C, + ("n", "e"): 0x306D, + ("n", "o"): 0x306E, + ("h", "a"): 0x306F, + ("b", "a"): 0x3070, + ("p", "a"): 0x3071, + ("h", "i"): 0x3072, + ("b", "i"): 0x3073, + ("p", "i"): 0x3074, + ("h", "u"): 0x3075, + ("b", "u"): 0x3076, + ("p", "u"): 0x3077, + ("h", "e"): 0x3078, + ("b", "e"): 0x3079, + ("p", "e"): 0x307A, + ("h", "o"): 0x307B, + ("b", "o"): 0x307C, + ("p", "o"): 0x307D, + ("m", "a"): 0x307E, + ("m", "i"): 0x307F, + ("m", "u"): 0x3080, + ("m", "e"): 0x3081, + ("m", "o"): 0x3082, + ("y", "A"): 0x3083, + ("y", "a"): 0x3084, + ("y", "U"): 0x3085, + ("y", "u"): 0x3086, + ("y", "O"): 0x3087, + ("y", "o"): 0x3088, + ("r", "a"): 0x3089, + ("r", "i"): 0x308A, + ("r", "u"): 0x308B, + ("r", "e"): 0x308C, + ("r", "o"): 0x308D, + ("w", "A"): 0x308E, + ("w", "a"): 0x308F, + ("w", "i"): 0x3090, + ("w", "e"): 0x3091, + ("w", "o"): 0x3092, + ("n", "5"): 0x3093, + ("v", "u"): 0x3094, + ('"', "5"): 0x309B, + ("0", "5"): 0x309C, + ("*", "5"): 0x309D, + ("+", "5"): 0x309E, + ("a", "6"): 0x30A1, + ("A", "6"): 0x30A2, + ("i", "6"): 0x30A3, + ("I", "6"): 0x30A4, + ("u", "6"): 0x30A5, + ("U", "6"): 0x30A6, + ("e", "6"): 0x30A7, + ("E", "6"): 0x30A8, + ("o", "6"): 0x30A9, + ("O", "6"): 0x30AA, + ("K", "a"): 0x30AB, + ("G", "a"): 0x30AC, + ("K", "i"): 0x30AD, + ("G", "i"): 0x30AE, + ("K", "u"): 0x30AF, + ("G", "u"): 0x30B0, + ("K", "e"): 0x30B1, + ("G", "e"): 0x30B2, + ("K", "o"): 0x30B3, + ("G", "o"): 0x30B4, + ("S", "a"): 0x30B5, + ("Z", "a"): 0x30B6, + ("S", "i"): 0x30B7, + ("Z", "i"): 0x30B8, + ("S", "u"): 0x30B9, + ("Z", "u"): 0x30BA, + ("S", "e"): 0x30BB, + ("Z", "e"): 0x30BC, + ("S", "o"): 0x30BD, + ("Z", "o"): 0x30BE, + ("T", "a"): 0x30BF, + ("D", "a"): 0x30C0, + ("T", "i"): 0x30C1, + ("D", "i"): 0x30C2, + ("T", "U"): 0x30C3, + ("T", "u"): 0x30C4, + ("D", "u"): 0x30C5, + ("T", "e"): 0x30C6, + ("D", "e"): 0x30C7, + ("T", "o"): 0x30C8, + ("D", "o"): 0x30C9, + ("N", "a"): 0x30CA, + ("N", "i"): 0x30CB, + ("N", "u"): 0x30CC, + ("N", "e"): 0x30CD, + ("N", "o"): 0x30CE, + ("H", "a"): 0x30CF, + ("B", "a"): 0x30D0, + ("P", "a"): 0x30D1, + ("H", "i"): 0x30D2, + ("B", "i"): 0x30D3, + ("P", "i"): 0x30D4, + ("H", "u"): 0x30D5, + ("B", "u"): 0x30D6, + ("P", "u"): 0x30D7, + ("H", "e"): 0x30D8, + ("B", "e"): 0x30D9, + ("P", "e"): 0x30DA, + ("H", "o"): 0x30DB, + ("B", "o"): 0x30DC, + ("P", "o"): 0x30DD, + ("M", "a"): 0x30DE, + ("M", "i"): 0x30DF, + ("M", "u"): 0x30E0, + ("M", "e"): 0x30E1, + ("M", "o"): 0x30E2, + ("Y", "A"): 0x30E3, + ("Y", "a"): 0x30E4, + ("Y", "U"): 0x30E5, + ("Y", "u"): 0x30E6, + ("Y", "O"): 0x30E7, + ("Y", "o"): 0x30E8, + ("R", "a"): 0x30E9, + ("R", "i"): 0x30EA, + ("R", "u"): 0x30EB, + ("R", "e"): 0x30EC, + ("R", "o"): 0x30ED, + ("W", "A"): 0x30EE, + ("W", "a"): 0x30EF, + ("W", "i"): 0x30F0, + ("W", "e"): 0x30F1, + ("W", "o"): 0x30F2, + ("N", "6"): 0x30F3, + ("V", "u"): 0x30F4, + ("K", "A"): 0x30F5, + ("K", "E"): 0x30F6, + ("V", "a"): 0x30F7, + ("V", "i"): 0x30F8, + ("V", "e"): 0x30F9, + ("V", "o"): 0x30FA, + (".", "6"): 0x30FB, + ("-", "6"): 0x30FC, + ("*", "6"): 0x30FD, + ("+", "6"): 0x30FE, + ("b", "4"): 0x3105, + ("p", "4"): 0x3106, + ("m", "4"): 0x3107, + ("f", "4"): 0x3108, + ("d", "4"): 0x3109, + ("t", "4"): 0x310A, + ("n", "4"): 0x310B, + ("l", "4"): 0x310C, + ("g", "4"): 0x310D, + ("k", "4"): 0x310E, + ("h", "4"): 0x310F, + ("j", "4"): 0x3110, + ("q", "4"): 0x3111, + ("x", "4"): 0x3112, + ("z", "h"): 0x3113, + ("c", "h"): 0x3114, + ("s", "h"): 0x3115, + ("r", "4"): 0x3116, + ("z", "4"): 0x3117, + ("c", "4"): 0x3118, + ("s", "4"): 0x3119, + ("a", "4"): 0x311A, + ("o", "4"): 0x311B, + ("e", "4"): 0x311C, + ("a", "i"): 0x311E, + ("e", "i"): 0x311F, + ("a", "u"): 0x3120, + ("o", "u"): 0x3121, + ("a", "n"): 0x3122, + ("e", "n"): 0x3123, + ("a", "N"): 0x3124, + ("e", "N"): 0x3125, + ("e", "r"): 0x3126, + ("i", "4"): 0x3127, + ("u", "4"): 0x3128, + ("i", "u"): 0x3129, + ("v", "4"): 0x312A, + ("n", "G"): 0x312B, + ("g", "n"): 0x312C, + ("1", "c"): 0x3220, + ("2", "c"): 0x3221, + ("3", "c"): 0x3222, + ("4", "c"): 0x3223, + ("5", "c"): 0x3224, + ("6", "c"): 0x3225, + ("7", "c"): 0x3226, + ("8", "c"): 0x3227, + ("9", "c"): 0x3228, + # code points 0xe000 - 0xefff excluded, they have no assigned + # characters, only used in proposals. + ("f", "f"): 0xFB00, + ("f", "i"): 0xFB01, + ("f", "l"): 0xFB02, + ("f", "t"): 0xFB05, + ("s", "t"): 0xFB06, + # Vim 5.x compatible digraphs that don't conflict with the above + ("~", "!"): 161, + ("c", "|"): 162, + ("$", "$"): 163, + ("o", "x"): 164, # currency symbol in ISO 8859-1 + ("Y", "-"): 165, + ("|", "|"): 166, + ("c", "O"): 169, + ("-", ","): 172, + ("-", "="): 175, + ("~", "o"): 176, + ("2", "2"): 178, + ("3", "3"): 179, + ("p", "p"): 182, + ("~", "."): 183, + ("1", "1"): 185, + ("~", "?"): 191, + ("A", "`"): 192, + ("A", "^"): 194, + ("A", "~"): 195, + ("A", '"'): 196, + ("A", "@"): 197, + ("E", "`"): 200, + ("E", "^"): 202, + ("E", '"'): 203, + ("I", "`"): 204, + ("I", "^"): 206, + ("I", '"'): 207, + ("N", "~"): 209, + ("O", "`"): 210, + ("O", "^"): 212, + ("O", "~"): 213, + ("/", "\\"): 215, # multiplication symbol in ISO 8859-1 + ("U", "`"): 217, + ("U", "^"): 219, + ("I", "p"): 222, + ("a", "`"): 224, + ("a", "^"): 226, + ("a", "~"): 227, + ("a", '"'): 228, + ("a", "@"): 229, + ("e", "`"): 232, + ("e", "^"): 234, + ("e", '"'): 235, + ("i", "`"): 236, + ("i", "^"): 238, + ("n", "~"): 241, + ("o", "`"): 242, + ("o", "^"): 244, + ("o", "~"): 245, + ("u", "`"): 249, + ("u", "^"): 251, + ("y", '"'): 255, +} diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/emacs_state.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/emacs_state.py index 77cb1cd12e..4c996224a0 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/emacs_state.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/emacs_state.py @@ -1,36 +1,36 @@ -from typing import List, Optional - -from .key_processor import KeyPress - -__all__ = [ - "EmacsState", -] - - -class EmacsState: - """ - Mutable class to hold Emacs specific state. - """ - - def __init__(self) -> None: - # Simple macro recording. (Like Readline does.) - # (For Emacs mode.) - self.macro: Optional[List[KeyPress]] = [] - self.current_recording: Optional[List[KeyPress]] = None - - def reset(self) -> None: - self.current_recording = None - - @property - def is_recording(self) -> bool: - "Tell whether we are recording a macro." - return self.current_recording is not None - - def start_macro(self) -> None: - "Start recording macro." - self.current_recording = [] - - def end_macro(self) -> None: - "End recording macro." - self.macro = self.current_recording - self.current_recording = None +from typing import List, Optional + +from .key_processor import KeyPress + +__all__ = [ + "EmacsState", +] + + +class EmacsState: + """ + Mutable class to hold Emacs specific state. + """ + + def __init__(self) -> None: + # Simple macro recording. (Like Readline does.) + # (For Emacs mode.) + self.macro: Optional[List[KeyPress]] = [] + self.current_recording: Optional[List[KeyPress]] = None + + def reset(self) -> None: + self.current_recording = None + + @property + def is_recording(self) -> bool: + "Tell whether we are recording a macro." + return self.current_recording is not None + + def start_macro(self) -> None: + "Start recording macro." + self.current_recording = [] + + def end_macro(self) -> None: + "End recording macro." + self.macro = self.current_recording + self.current_recording = None diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/key_bindings.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/key_bindings.py index dc8c5d356b..06ca376b09 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/key_bindings.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/key_bindings.py @@ -1,672 +1,672 @@ -""" -Key bindings registry. - -A `KeyBindings` object is a container that holds a list of key bindings. It has a -very efficient internal data structure for checking which key bindings apply -for a pressed key. - -Typical usage:: - - kb = KeyBindings() - - @kb.add(Keys.ControlX, Keys.ControlC, filter=INSERT) - def handler(event): - # Handle ControlX-ControlC key sequence. - pass - -It is also possible to combine multiple KeyBindings objects. We do this in the -default key bindings. There are some KeyBindings objects that contain the Emacs -bindings, while others contain the Vi bindings. They are merged together using -`merge_key_bindings`. - -We also have a `ConditionalKeyBindings` object that can enable/disable a group of -key bindings at once. - - -It is also possible to add a filter to a function, before a key binding has -been assigned, through the `key_binding` decorator.:: - - # First define a key handler with the `filter`. - @key_binding(filter=condition) - def my_key_binding(event): - ... - - # Later, add it to the key bindings. - kb.add(Keys.A, my_key_binding) -""" -from abc import ABCMeta, abstractmethod, abstractproperty -from inspect import isawaitable -from typing import ( - TYPE_CHECKING, - Awaitable, - Callable, - Hashable, - List, - Optional, - Sequence, - Tuple, - TypeVar, - Union, - cast, -) - -from prompt_toolkit.cache import SimpleCache -from prompt_toolkit.filters import FilterOrBool, Never, to_filter -from prompt_toolkit.keys import KEY_ALIASES, Keys - -if TYPE_CHECKING: - # Avoid circular imports. - from .key_processor import KeyPressEvent - - # The only two return values for a mouse hander (and key bindings) are - # `None` and `NotImplemented`. For the type checker it's best to annotate - # this as `object`. (The consumer never expects a more specific instance: - # checking for NotImplemented can be done using `is NotImplemented`.) - NotImplementedOrNone = object - # Other non-working options are: - # * Optional[Literal[NotImplemented]] - # --> Doesn't work, Literal can't take an Any. - # * None - # --> Doesn't work. We can't assign the result of a function that - # returns `None` to a variable. - # * Any - # --> Works, but too broad. - - -__all__ = [ - "NotImplementedOrNone", - "Binding", - "KeyBindingsBase", - "KeyBindings", - "ConditionalKeyBindings", - "merge_key_bindings", - "DynamicKeyBindings", - "GlobalOnlyKeyBindings", -] - -# Key bindings can be regular functions or coroutines. -# In both cases, if they return `NotImplemented`, the UI won't be invalidated. -# This is mainly used in case of mouse move events, to prevent excessive -# repainting during mouse move events. -KeyHandlerCallable = Callable[ - ["KeyPressEvent"], Union["NotImplementedOrNone", Awaitable["NotImplementedOrNone"]] -] - - -class Binding: - """ - Key binding: (key sequence + handler + filter). - (Immutable binding class.) - - :param record_in_macro: When True, don't record this key binding when a - macro is recorded. - """ - - def __init__( - self, - keys: Tuple[Union[Keys, str], ...], - handler: KeyHandlerCallable, - filter: FilterOrBool = True, - eager: FilterOrBool = False, - is_global: FilterOrBool = False, - save_before: Callable[["KeyPressEvent"], bool] = (lambda e: True), - record_in_macro: FilterOrBool = True, - ) -> None: - self.keys = keys - self.handler = handler - self.filter = to_filter(filter) - self.eager = to_filter(eager) - self.is_global = to_filter(is_global) - self.save_before = save_before - self.record_in_macro = to_filter(record_in_macro) - - def call(self, event: "KeyPressEvent") -> None: - result = self.handler(event) - - # If the handler is a coroutine, create an asyncio task. - if isawaitable(result): - awaitable = cast(Awaitable["NotImplementedOrNone"], result) - - async def bg_task() -> None: - result = await awaitable - if result != NotImplemented: - event.app.invalidate() - - event.app.create_background_task(bg_task()) - - elif result != NotImplemented: - event.app.invalidate() - - def __repr__(self) -> str: - return "%s(keys=%r, handler=%r)" % ( - self.__class__.__name__, - self.keys, - self.handler, - ) - - -# Sequence of keys presses. -KeysTuple = Tuple[Union[Keys, str], ...] - - -class KeyBindingsBase(metaclass=ABCMeta): - """ - Interface for a KeyBindings. - """ - - @abstractproperty - def _version(self) -> Hashable: - """ - For cache invalidation. - This should increase every time that - something changes. - """ - return 0 - - @abstractmethod - def get_bindings_for_keys(self, keys: KeysTuple) -> List[Binding]: - """ - Return a list of key bindings that can handle these keys. - (This return also inactive bindings, so the `filter` still has to be - called, for checking it.) - - :param keys: tuple of keys. - """ - return [] - - @abstractmethod - def get_bindings_starting_with_keys(self, keys: KeysTuple) -> List[Binding]: - """ - Return a list of key bindings that handle a key sequence starting with - `keys`. (It does only return bindings for which the sequences are - longer than `keys`. And like `get_bindings_for_keys`, it also includes - inactive bindings.) - - :param keys: tuple of keys. - """ - return [] - - @abstractproperty - def bindings(self) -> List[Binding]: - """ - List of `Binding` objects. - (These need to be exposed, so that `KeyBindings` objects can be merged - together.) - """ - return [] - - # `add` and `remove` don't have to be part of this interface. - - -T = TypeVar("T", bound=Union[KeyHandlerCallable, Binding]) - - -class KeyBindings(KeyBindingsBase): - """ - A container for a set of key bindings. - - Example usage:: - - kb = KeyBindings() - - @kb.add('c-t') - def _(event): - print('Control-T pressed') - - @kb.add('c-a', 'c-b') - def _(event): - print('Control-A pressed, followed by Control-B') - - @kb.add('c-x', filter=is_searching) - def _(event): - print('Control-X pressed') # Works only if we are searching. - - """ - - def __init__(self) -> None: - self._bindings: List[Binding] = [] - self._get_bindings_for_keys_cache: SimpleCache[ - KeysTuple, List[Binding] - ] = SimpleCache(maxsize=10000) - self._get_bindings_starting_with_keys_cache: SimpleCache[ - KeysTuple, List[Binding] - ] = SimpleCache(maxsize=1000) - self.__version = 0 # For cache invalidation. - - def _clear_cache(self) -> None: - self.__version += 1 - self._get_bindings_for_keys_cache.clear() - self._get_bindings_starting_with_keys_cache.clear() - - @property - def bindings(self) -> List[Binding]: - return self._bindings - - @property - def _version(self) -> Hashable: - return self.__version - - def add( - self, - *keys: Union[Keys, str], - filter: FilterOrBool = True, - eager: FilterOrBool = False, - is_global: FilterOrBool = False, - save_before: Callable[["KeyPressEvent"], bool] = (lambda e: True), - record_in_macro: FilterOrBool = True, - ) -> Callable[[T], T]: - """ - Decorator for adding a key bindings. - - :param filter: :class:`~prompt_toolkit.filters.Filter` to determine - when this key binding is active. - :param eager: :class:`~prompt_toolkit.filters.Filter` or `bool`. - When True, ignore potential longer matches when this key binding is - hit. E.g. when there is an active eager key binding for Ctrl-X, - execute the handler immediately and ignore the key binding for - Ctrl-X Ctrl-E of which it is a prefix. - :param is_global: When this key bindings is added to a `Container` or - `Control`, make it a global (always active) binding. - :param save_before: Callable that takes an `Event` and returns True if - we should save the current buffer, before handling the event. - (That's the default.) - :param record_in_macro: Record these key bindings when a macro is - being recorded. (True by default.) - """ - assert keys - - keys = tuple(_parse_key(k) for k in keys) - - if isinstance(filter, Never): - # When a filter is Never, it will always stay disabled, so in that - # case don't bother putting it in the key bindings. It will slow - # down every key press otherwise. - def decorator(func: T) -> T: - return func - - else: - - def decorator(func: T) -> T: - if isinstance(func, Binding): - # We're adding an existing Binding object. - self.bindings.append( - Binding( - keys, - func.handler, - filter=func.filter & to_filter(filter), - eager=to_filter(eager) | func.eager, - is_global=to_filter(is_global) | func.is_global, - save_before=func.save_before, - record_in_macro=func.record_in_macro, - ) - ) - else: - self.bindings.append( - Binding( - keys, - cast(KeyHandlerCallable, func), - filter=filter, - eager=eager, - is_global=is_global, - save_before=save_before, - record_in_macro=record_in_macro, - ) - ) - self._clear_cache() - - return func - - return decorator - - def remove(self, *args: Union[Keys, str, KeyHandlerCallable]) -> None: - """ - Remove a key binding. - - This expects either a function that was given to `add` method as - parameter or a sequence of key bindings. - - Raises `ValueError` when no bindings was found. - - Usage:: - - remove(handler) # Pass handler. - remove('c-x', 'c-a') # Or pass the key bindings. - """ - found = False - - if callable(args[0]): - assert len(args) == 1 - function = args[0] - - # Remove the given function. - for b in self.bindings: - if b.handler == function: - self.bindings.remove(b) - found = True - - else: - assert len(args) > 0 - args = cast(Tuple[Union[Keys, str]], args) - - # Remove this sequence of key bindings. - keys = tuple(_parse_key(k) for k in args) - - for b in self.bindings: - if b.keys == keys: - self.bindings.remove(b) - found = True - - if found: - self._clear_cache() - else: - # No key binding found for this function. Raise ValueError. - raise ValueError("Binding not found: %r" % (function,)) - - # For backwards-compatibility. - add_binding = add - remove_binding = remove - - def get_bindings_for_keys(self, keys: KeysTuple) -> List[Binding]: - """ - Return a list of key bindings that can handle this key. - (This return also inactive bindings, so the `filter` still has to be - called, for checking it.) - - :param keys: tuple of keys. - """ - - def get() -> List[Binding]: - result: List[Tuple[int, Binding]] = [] - - for b in self.bindings: - if len(keys) == len(b.keys): - match = True - any_count = 0 - - for i, j in zip(b.keys, keys): - if i != j and i != Keys.Any: - match = False - break - - if i == Keys.Any: - any_count += 1 - - if match: - result.append((any_count, b)) - - # Place bindings that have more 'Any' occurrences in them at the end. - result = sorted(result, key=lambda item: -item[0]) - - return [item[1] for item in result] - - return self._get_bindings_for_keys_cache.get(keys, get) - - def get_bindings_starting_with_keys(self, keys: KeysTuple) -> List[Binding]: - """ - Return a list of key bindings that handle a key sequence starting with - `keys`. (It does only return bindings for which the sequences are - longer than `keys`. And like `get_bindings_for_keys`, it also includes - inactive bindings.) - - :param keys: tuple of keys. - """ - - def get() -> List[Binding]: - result = [] - for b in self.bindings: - if len(keys) < len(b.keys): - match = True - for i, j in zip(b.keys, keys): - if i != j and i != Keys.Any: - match = False - break - if match: - result.append(b) - return result - - return self._get_bindings_starting_with_keys_cache.get(keys, get) - - -def _parse_key(key: Union[Keys, str]) -> Union[str, Keys]: - """ - Replace key by alias and verify whether it's a valid one. - """ - # Already a parse key? -> Return it. - if isinstance(key, Keys): - return key - - # Lookup aliases. - key = KEY_ALIASES.get(key, key) - - # Replace 'space' by ' ' - if key == "space": - key = " " - - # Return as `Key` object when it's a special key. - try: - return Keys(key) - except ValueError: - pass - - # Final validation. - if len(key) != 1: - raise ValueError("Invalid key: %s" % (key,)) - - return key - - -def key_binding( - filter: FilterOrBool = True, - eager: FilterOrBool = False, - is_global: FilterOrBool = False, - save_before: Callable[["KeyPressEvent"], bool] = (lambda event: True), - record_in_macro: FilterOrBool = True, -) -> Callable[[KeyHandlerCallable], Binding]: - """ - Decorator that turn a function into a `Binding` object. This can be added - to a `KeyBindings` object when a key binding is assigned. - """ - assert save_before is None or callable(save_before) - - filter = to_filter(filter) - eager = to_filter(eager) - is_global = to_filter(is_global) - save_before = save_before - record_in_macro = to_filter(record_in_macro) - keys = () - - def decorator(function: KeyHandlerCallable) -> Binding: - return Binding( - keys, - function, - filter=filter, - eager=eager, - is_global=is_global, - save_before=save_before, - record_in_macro=record_in_macro, - ) - - return decorator - - -class _Proxy(KeyBindingsBase): - """ - Common part for ConditionalKeyBindings and _MergedKeyBindings. - """ - - def __init__(self) -> None: - # `KeyBindings` to be synchronized with all the others. - self._bindings2: KeyBindingsBase = KeyBindings() - self._last_version: Hashable = () - - def _update_cache(self) -> None: - """ - If `self._last_version` is outdated, then this should update - the version and `self._bindings2`. - """ - raise NotImplementedError - - # Proxy methods to self._bindings2. - - @property - def bindings(self) -> List[Binding]: - self._update_cache() - return self._bindings2.bindings - - @property - def _version(self) -> Hashable: - self._update_cache() - return self._last_version - - def get_bindings_for_keys(self, keys: KeysTuple) -> List[Binding]: - self._update_cache() - return self._bindings2.get_bindings_for_keys(keys) - - def get_bindings_starting_with_keys(self, keys: KeysTuple) -> List[Binding]: - self._update_cache() - return self._bindings2.get_bindings_starting_with_keys(keys) - - -class ConditionalKeyBindings(_Proxy): - """ - Wraps around a `KeyBindings`. Disable/enable all the key bindings according to - the given (additional) filter.:: - - @Condition - def setting_is_true(): - return True # or False - - registry = ConditionalKeyBindings(key_bindings, setting_is_true) - - When new key bindings are added to this object. They are also - enable/disabled according to the given `filter`. - - :param registries: List of :class:`.KeyBindings` objects. - :param filter: :class:`~prompt_toolkit.filters.Filter` object. - """ - - def __init__( - self, key_bindings: KeyBindingsBase, filter: FilterOrBool = True - ) -> None: - - _Proxy.__init__(self) - - self.key_bindings = key_bindings - self.filter = to_filter(filter) - - def _update_cache(self) -> None: - "If the original key bindings was changed. Update our copy version." - expected_version = self.key_bindings._version - - if self._last_version != expected_version: - bindings2 = KeyBindings() - - # Copy all bindings from `self.key_bindings`, adding our condition. - for b in self.key_bindings.bindings: - bindings2.bindings.append( - Binding( - keys=b.keys, - handler=b.handler, - filter=self.filter & b.filter, - eager=b.eager, - is_global=b.is_global, - save_before=b.save_before, - record_in_macro=b.record_in_macro, - ) - ) - - self._bindings2 = bindings2 - self._last_version = expected_version - - -class _MergedKeyBindings(_Proxy): - """ - Merge multiple registries of key bindings into one. - - This class acts as a proxy to multiple :class:`.KeyBindings` objects, but - behaves as if this is just one bigger :class:`.KeyBindings`. - - :param registries: List of :class:`.KeyBindings` objects. - """ - - def __init__(self, registries: Sequence[KeyBindingsBase]) -> None: - _Proxy.__init__(self) - self.registries = registries - - def _update_cache(self) -> None: - """ - If one of the original registries was changed. Update our merged - version. - """ - expected_version = tuple(r._version for r in self.registries) - - if self._last_version != expected_version: - bindings2 = KeyBindings() - - for reg in self.registries: - bindings2.bindings.extend(reg.bindings) - - self._bindings2 = bindings2 - self._last_version = expected_version - - -def merge_key_bindings(bindings: Sequence[KeyBindingsBase]) -> _MergedKeyBindings: - """ - Merge multiple :class:`.Keybinding` objects together. - - Usage:: - - bindings = merge_key_bindings([bindings1, bindings2, ...]) - """ - return _MergedKeyBindings(bindings) - - -class DynamicKeyBindings(_Proxy): - """ - KeyBindings class that can dynamically returns any KeyBindings. - - :param get_key_bindings: Callable that returns a :class:`.KeyBindings` instance. - """ - - def __init__( - self, get_key_bindings: Callable[[], Optional[KeyBindingsBase]] - ) -> None: - self.get_key_bindings = get_key_bindings - self.__version = 0 - self._last_child_version = None - self._dummy = KeyBindings() # Empty key bindings. - - def _update_cache(self) -> None: - key_bindings = self.get_key_bindings() or self._dummy - assert isinstance(key_bindings, KeyBindingsBase) - version = id(key_bindings), key_bindings._version - - self._bindings2 = key_bindings - self._last_version = version - - -class GlobalOnlyKeyBindings(_Proxy): - """ - Wrapper around a :class:`.KeyBindings` object that only exposes the global - key bindings. - """ - - def __init__(self, key_bindings: KeyBindingsBase) -> None: - _Proxy.__init__(self) - self.key_bindings = key_bindings - - def _update_cache(self) -> None: - """ - If one of the original registries was changed. Update our merged - version. - """ - expected_version = self.key_bindings._version - - if self._last_version != expected_version: - bindings2 = KeyBindings() - - for b in self.key_bindings.bindings: - if b.is_global(): - bindings2.bindings.append(b) - - self._bindings2 = bindings2 - self._last_version = expected_version +""" +Key bindings registry. + +A `KeyBindings` object is a container that holds a list of key bindings. It has a +very efficient internal data structure for checking which key bindings apply +for a pressed key. + +Typical usage:: + + kb = KeyBindings() + + @kb.add(Keys.ControlX, Keys.ControlC, filter=INSERT) + def handler(event): + # Handle ControlX-ControlC key sequence. + pass + +It is also possible to combine multiple KeyBindings objects. We do this in the +default key bindings. There are some KeyBindings objects that contain the Emacs +bindings, while others contain the Vi bindings. They are merged together using +`merge_key_bindings`. + +We also have a `ConditionalKeyBindings` object that can enable/disable a group of +key bindings at once. + + +It is also possible to add a filter to a function, before a key binding has +been assigned, through the `key_binding` decorator.:: + + # First define a key handler with the `filter`. + @key_binding(filter=condition) + def my_key_binding(event): + ... + + # Later, add it to the key bindings. + kb.add(Keys.A, my_key_binding) +""" +from abc import ABCMeta, abstractmethod, abstractproperty +from inspect import isawaitable +from typing import ( + TYPE_CHECKING, + Awaitable, + Callable, + Hashable, + List, + Optional, + Sequence, + Tuple, + TypeVar, + Union, + cast, +) + +from prompt_toolkit.cache import SimpleCache +from prompt_toolkit.filters import FilterOrBool, Never, to_filter +from prompt_toolkit.keys import KEY_ALIASES, Keys + +if TYPE_CHECKING: + # Avoid circular imports. + from .key_processor import KeyPressEvent + + # The only two return values for a mouse hander (and key bindings) are + # `None` and `NotImplemented`. For the type checker it's best to annotate + # this as `object`. (The consumer never expects a more specific instance: + # checking for NotImplemented can be done using `is NotImplemented`.) + NotImplementedOrNone = object + # Other non-working options are: + # * Optional[Literal[NotImplemented]] + # --> Doesn't work, Literal can't take an Any. + # * None + # --> Doesn't work. We can't assign the result of a function that + # returns `None` to a variable. + # * Any + # --> Works, but too broad. + + +__all__ = [ + "NotImplementedOrNone", + "Binding", + "KeyBindingsBase", + "KeyBindings", + "ConditionalKeyBindings", + "merge_key_bindings", + "DynamicKeyBindings", + "GlobalOnlyKeyBindings", +] + +# Key bindings can be regular functions or coroutines. +# In both cases, if they return `NotImplemented`, the UI won't be invalidated. +# This is mainly used in case of mouse move events, to prevent excessive +# repainting during mouse move events. +KeyHandlerCallable = Callable[ + ["KeyPressEvent"], Union["NotImplementedOrNone", Awaitable["NotImplementedOrNone"]] +] + + +class Binding: + """ + Key binding: (key sequence + handler + filter). + (Immutable binding class.) + + :param record_in_macro: When True, don't record this key binding when a + macro is recorded. + """ + + def __init__( + self, + keys: Tuple[Union[Keys, str], ...], + handler: KeyHandlerCallable, + filter: FilterOrBool = True, + eager: FilterOrBool = False, + is_global: FilterOrBool = False, + save_before: Callable[["KeyPressEvent"], bool] = (lambda e: True), + record_in_macro: FilterOrBool = True, + ) -> None: + self.keys = keys + self.handler = handler + self.filter = to_filter(filter) + self.eager = to_filter(eager) + self.is_global = to_filter(is_global) + self.save_before = save_before + self.record_in_macro = to_filter(record_in_macro) + + def call(self, event: "KeyPressEvent") -> None: + result = self.handler(event) + + # If the handler is a coroutine, create an asyncio task. + if isawaitable(result): + awaitable = cast(Awaitable["NotImplementedOrNone"], result) + + async def bg_task() -> None: + result = await awaitable + if result != NotImplemented: + event.app.invalidate() + + event.app.create_background_task(bg_task()) + + elif result != NotImplemented: + event.app.invalidate() + + def __repr__(self) -> str: + return "%s(keys=%r, handler=%r)" % ( + self.__class__.__name__, + self.keys, + self.handler, + ) + + +# Sequence of keys presses. +KeysTuple = Tuple[Union[Keys, str], ...] + + +class KeyBindingsBase(metaclass=ABCMeta): + """ + Interface for a KeyBindings. + """ + + @abstractproperty + def _version(self) -> Hashable: + """ + For cache invalidation. - This should increase every time that + something changes. + """ + return 0 + + @abstractmethod + def get_bindings_for_keys(self, keys: KeysTuple) -> List[Binding]: + """ + Return a list of key bindings that can handle these keys. + (This return also inactive bindings, so the `filter` still has to be + called, for checking it.) + + :param keys: tuple of keys. + """ + return [] + + @abstractmethod + def get_bindings_starting_with_keys(self, keys: KeysTuple) -> List[Binding]: + """ + Return a list of key bindings that handle a key sequence starting with + `keys`. (It does only return bindings for which the sequences are + longer than `keys`. And like `get_bindings_for_keys`, it also includes + inactive bindings.) + + :param keys: tuple of keys. + """ + return [] + + @abstractproperty + def bindings(self) -> List[Binding]: + """ + List of `Binding` objects. + (These need to be exposed, so that `KeyBindings` objects can be merged + together.) + """ + return [] + + # `add` and `remove` don't have to be part of this interface. + + +T = TypeVar("T", bound=Union[KeyHandlerCallable, Binding]) + + +class KeyBindings(KeyBindingsBase): + """ + A container for a set of key bindings. + + Example usage:: + + kb = KeyBindings() + + @kb.add('c-t') + def _(event): + print('Control-T pressed') + + @kb.add('c-a', 'c-b') + def _(event): + print('Control-A pressed, followed by Control-B') + + @kb.add('c-x', filter=is_searching) + def _(event): + print('Control-X pressed') # Works only if we are searching. + + """ + + def __init__(self) -> None: + self._bindings: List[Binding] = [] + self._get_bindings_for_keys_cache: SimpleCache[ + KeysTuple, List[Binding] + ] = SimpleCache(maxsize=10000) + self._get_bindings_starting_with_keys_cache: SimpleCache[ + KeysTuple, List[Binding] + ] = SimpleCache(maxsize=1000) + self.__version = 0 # For cache invalidation. + + def _clear_cache(self) -> None: + self.__version += 1 + self._get_bindings_for_keys_cache.clear() + self._get_bindings_starting_with_keys_cache.clear() + + @property + def bindings(self) -> List[Binding]: + return self._bindings + + @property + def _version(self) -> Hashable: + return self.__version + + def add( + self, + *keys: Union[Keys, str], + filter: FilterOrBool = True, + eager: FilterOrBool = False, + is_global: FilterOrBool = False, + save_before: Callable[["KeyPressEvent"], bool] = (lambda e: True), + record_in_macro: FilterOrBool = True, + ) -> Callable[[T], T]: + """ + Decorator for adding a key bindings. + + :param filter: :class:`~prompt_toolkit.filters.Filter` to determine + when this key binding is active. + :param eager: :class:`~prompt_toolkit.filters.Filter` or `bool`. + When True, ignore potential longer matches when this key binding is + hit. E.g. when there is an active eager key binding for Ctrl-X, + execute the handler immediately and ignore the key binding for + Ctrl-X Ctrl-E of which it is a prefix. + :param is_global: When this key bindings is added to a `Container` or + `Control`, make it a global (always active) binding. + :param save_before: Callable that takes an `Event` and returns True if + we should save the current buffer, before handling the event. + (That's the default.) + :param record_in_macro: Record these key bindings when a macro is + being recorded. (True by default.) + """ + assert keys + + keys = tuple(_parse_key(k) for k in keys) + + if isinstance(filter, Never): + # When a filter is Never, it will always stay disabled, so in that + # case don't bother putting it in the key bindings. It will slow + # down every key press otherwise. + def decorator(func: T) -> T: + return func + + else: + + def decorator(func: T) -> T: + if isinstance(func, Binding): + # We're adding an existing Binding object. + self.bindings.append( + Binding( + keys, + func.handler, + filter=func.filter & to_filter(filter), + eager=to_filter(eager) | func.eager, + is_global=to_filter(is_global) | func.is_global, + save_before=func.save_before, + record_in_macro=func.record_in_macro, + ) + ) + else: + self.bindings.append( + Binding( + keys, + cast(KeyHandlerCallable, func), + filter=filter, + eager=eager, + is_global=is_global, + save_before=save_before, + record_in_macro=record_in_macro, + ) + ) + self._clear_cache() + + return func + + return decorator + + def remove(self, *args: Union[Keys, str, KeyHandlerCallable]) -> None: + """ + Remove a key binding. + + This expects either a function that was given to `add` method as + parameter or a sequence of key bindings. + + Raises `ValueError` when no bindings was found. + + Usage:: + + remove(handler) # Pass handler. + remove('c-x', 'c-a') # Or pass the key bindings. + """ + found = False + + if callable(args[0]): + assert len(args) == 1 + function = args[0] + + # Remove the given function. + for b in self.bindings: + if b.handler == function: + self.bindings.remove(b) + found = True + + else: + assert len(args) > 0 + args = cast(Tuple[Union[Keys, str]], args) + + # Remove this sequence of key bindings. + keys = tuple(_parse_key(k) for k in args) + + for b in self.bindings: + if b.keys == keys: + self.bindings.remove(b) + found = True + + if found: + self._clear_cache() + else: + # No key binding found for this function. Raise ValueError. + raise ValueError("Binding not found: %r" % (function,)) + + # For backwards-compatibility. + add_binding = add + remove_binding = remove + + def get_bindings_for_keys(self, keys: KeysTuple) -> List[Binding]: + """ + Return a list of key bindings that can handle this key. + (This return also inactive bindings, so the `filter` still has to be + called, for checking it.) + + :param keys: tuple of keys. + """ + + def get() -> List[Binding]: + result: List[Tuple[int, Binding]] = [] + + for b in self.bindings: + if len(keys) == len(b.keys): + match = True + any_count = 0 + + for i, j in zip(b.keys, keys): + if i != j and i != Keys.Any: + match = False + break + + if i == Keys.Any: + any_count += 1 + + if match: + result.append((any_count, b)) + + # Place bindings that have more 'Any' occurrences in them at the end. + result = sorted(result, key=lambda item: -item[0]) + + return [item[1] for item in result] + + return self._get_bindings_for_keys_cache.get(keys, get) + + def get_bindings_starting_with_keys(self, keys: KeysTuple) -> List[Binding]: + """ + Return a list of key bindings that handle a key sequence starting with + `keys`. (It does only return bindings for which the sequences are + longer than `keys`. And like `get_bindings_for_keys`, it also includes + inactive bindings.) + + :param keys: tuple of keys. + """ + + def get() -> List[Binding]: + result = [] + for b in self.bindings: + if len(keys) < len(b.keys): + match = True + for i, j in zip(b.keys, keys): + if i != j and i != Keys.Any: + match = False + break + if match: + result.append(b) + return result + + return self._get_bindings_starting_with_keys_cache.get(keys, get) + + +def _parse_key(key: Union[Keys, str]) -> Union[str, Keys]: + """ + Replace key by alias and verify whether it's a valid one. + """ + # Already a parse key? -> Return it. + if isinstance(key, Keys): + return key + + # Lookup aliases. + key = KEY_ALIASES.get(key, key) + + # Replace 'space' by ' ' + if key == "space": + key = " " + + # Return as `Key` object when it's a special key. + try: + return Keys(key) + except ValueError: + pass + + # Final validation. + if len(key) != 1: + raise ValueError("Invalid key: %s" % (key,)) + + return key + + +def key_binding( + filter: FilterOrBool = True, + eager: FilterOrBool = False, + is_global: FilterOrBool = False, + save_before: Callable[["KeyPressEvent"], bool] = (lambda event: True), + record_in_macro: FilterOrBool = True, +) -> Callable[[KeyHandlerCallable], Binding]: + """ + Decorator that turn a function into a `Binding` object. This can be added + to a `KeyBindings` object when a key binding is assigned. + """ + assert save_before is None or callable(save_before) + + filter = to_filter(filter) + eager = to_filter(eager) + is_global = to_filter(is_global) + save_before = save_before + record_in_macro = to_filter(record_in_macro) + keys = () + + def decorator(function: KeyHandlerCallable) -> Binding: + return Binding( + keys, + function, + filter=filter, + eager=eager, + is_global=is_global, + save_before=save_before, + record_in_macro=record_in_macro, + ) + + return decorator + + +class _Proxy(KeyBindingsBase): + """ + Common part for ConditionalKeyBindings and _MergedKeyBindings. + """ + + def __init__(self) -> None: + # `KeyBindings` to be synchronized with all the others. + self._bindings2: KeyBindingsBase = KeyBindings() + self._last_version: Hashable = () + + def _update_cache(self) -> None: + """ + If `self._last_version` is outdated, then this should update + the version and `self._bindings2`. + """ + raise NotImplementedError + + # Proxy methods to self._bindings2. + + @property + def bindings(self) -> List[Binding]: + self._update_cache() + return self._bindings2.bindings + + @property + def _version(self) -> Hashable: + self._update_cache() + return self._last_version + + def get_bindings_for_keys(self, keys: KeysTuple) -> List[Binding]: + self._update_cache() + return self._bindings2.get_bindings_for_keys(keys) + + def get_bindings_starting_with_keys(self, keys: KeysTuple) -> List[Binding]: + self._update_cache() + return self._bindings2.get_bindings_starting_with_keys(keys) + + +class ConditionalKeyBindings(_Proxy): + """ + Wraps around a `KeyBindings`. Disable/enable all the key bindings according to + the given (additional) filter.:: + + @Condition + def setting_is_true(): + return True # or False + + registry = ConditionalKeyBindings(key_bindings, setting_is_true) + + When new key bindings are added to this object. They are also + enable/disabled according to the given `filter`. + + :param registries: List of :class:`.KeyBindings` objects. + :param filter: :class:`~prompt_toolkit.filters.Filter` object. + """ + + def __init__( + self, key_bindings: KeyBindingsBase, filter: FilterOrBool = True + ) -> None: + + _Proxy.__init__(self) + + self.key_bindings = key_bindings + self.filter = to_filter(filter) + + def _update_cache(self) -> None: + "If the original key bindings was changed. Update our copy version." + expected_version = self.key_bindings._version + + if self._last_version != expected_version: + bindings2 = KeyBindings() + + # Copy all bindings from `self.key_bindings`, adding our condition. + for b in self.key_bindings.bindings: + bindings2.bindings.append( + Binding( + keys=b.keys, + handler=b.handler, + filter=self.filter & b.filter, + eager=b.eager, + is_global=b.is_global, + save_before=b.save_before, + record_in_macro=b.record_in_macro, + ) + ) + + self._bindings2 = bindings2 + self._last_version = expected_version + + +class _MergedKeyBindings(_Proxy): + """ + Merge multiple registries of key bindings into one. + + This class acts as a proxy to multiple :class:`.KeyBindings` objects, but + behaves as if this is just one bigger :class:`.KeyBindings`. + + :param registries: List of :class:`.KeyBindings` objects. + """ + + def __init__(self, registries: Sequence[KeyBindingsBase]) -> None: + _Proxy.__init__(self) + self.registries = registries + + def _update_cache(self) -> None: + """ + If one of the original registries was changed. Update our merged + version. + """ + expected_version = tuple(r._version for r in self.registries) + + if self._last_version != expected_version: + bindings2 = KeyBindings() + + for reg in self.registries: + bindings2.bindings.extend(reg.bindings) + + self._bindings2 = bindings2 + self._last_version = expected_version + + +def merge_key_bindings(bindings: Sequence[KeyBindingsBase]) -> _MergedKeyBindings: + """ + Merge multiple :class:`.Keybinding` objects together. + + Usage:: + + bindings = merge_key_bindings([bindings1, bindings2, ...]) + """ + return _MergedKeyBindings(bindings) + + +class DynamicKeyBindings(_Proxy): + """ + KeyBindings class that can dynamically returns any KeyBindings. + + :param get_key_bindings: Callable that returns a :class:`.KeyBindings` instance. + """ + + def __init__( + self, get_key_bindings: Callable[[], Optional[KeyBindingsBase]] + ) -> None: + self.get_key_bindings = get_key_bindings + self.__version = 0 + self._last_child_version = None + self._dummy = KeyBindings() # Empty key bindings. + + def _update_cache(self) -> None: + key_bindings = self.get_key_bindings() or self._dummy + assert isinstance(key_bindings, KeyBindingsBase) + version = id(key_bindings), key_bindings._version + + self._bindings2 = key_bindings + self._last_version = version + + +class GlobalOnlyKeyBindings(_Proxy): + """ + Wrapper around a :class:`.KeyBindings` object that only exposes the global + key bindings. + """ + + def __init__(self, key_bindings: KeyBindingsBase) -> None: + _Proxy.__init__(self) + self.key_bindings = key_bindings + + def _update_cache(self) -> None: + """ + If one of the original registries was changed. Update our merged + version. + """ + expected_version = self.key_bindings._version + + if self._last_version != expected_version: + bindings2 = KeyBindings() + + for b in self.key_bindings.bindings: + if b.is_global(): + bindings2.bindings.append(b) + + self._bindings2 = bindings2 + self._last_version = expected_version diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/key_processor.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/key_processor.py index 9ad09c0fa0..476393c1ee 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/key_processor.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/key_processor.py @@ -1,529 +1,529 @@ -# *** encoding: utf-8 *** -""" -An :class:`~.KeyProcessor` receives callbacks for the keystrokes parsed from -the input in the :class:`~prompt_toolkit.inputstream.InputStream` instance. - -The `KeyProcessor` will according to the implemented keybindings call the -correct callbacks when new key presses are feed through `feed`. -""" -import weakref -from asyncio import Task, sleep -from collections import deque -from typing import TYPE_CHECKING, Any, Deque, Generator, List, Optional, Union - -from prompt_toolkit.application.current import get_app -from prompt_toolkit.enums import EditingMode -from prompt_toolkit.filters.app import vi_navigation_mode -from prompt_toolkit.keys import Keys -from prompt_toolkit.utils import Event - -from .key_bindings import Binding, KeyBindingsBase - -if TYPE_CHECKING: - from prompt_toolkit.application import Application - from prompt_toolkit.buffer import Buffer - - -__all__ = [ - "KeyProcessor", - "KeyPress", - "KeyPressEvent", -] - - -class KeyPress: - """ - :param key: A `Keys` instance or text (one character). - :param data: The received string on stdin. (Often vt100 escape codes.) - """ - - def __init__(self, key: Union[Keys, str], data: Optional[str] = None) -> None: - assert isinstance(key, Keys) or len(key) == 1 - - if data is None: - if isinstance(key, Keys): - data = key.value - else: - data = key # 'key' is a one character string. - - self.key = key - self.data = data - - def __repr__(self) -> str: - return "%s(key=%r, data=%r)" % (self.__class__.__name__, self.key, self.data) - - def __eq__(self, other: object) -> bool: - if not isinstance(other, KeyPress): - return False - return self.key == other.key and self.data == other.data - - -""" -Helper object to indicate flush operation in the KeyProcessor. -NOTE: the implementation is very similar to the VT100 parser. -""" -_Flush = KeyPress("?", data="_Flush") - - -class KeyProcessor: - """ - Statemachine that receives :class:`KeyPress` instances and according to the - key bindings in the given :class:`KeyBindings`, calls the matching handlers. - - :: - - p = KeyProcessor(key_bindings) - - # Send keys into the processor. - p.feed(KeyPress(Keys.ControlX, '\x18')) - p.feed(KeyPress(Keys.ControlC, '\x03') - - # Process all the keys in the queue. - p.process_keys() - - # Now the ControlX-ControlC callback will be called if this sequence is - # registered in the key bindings. - - :param key_bindings: `KeyBindingsBase` instance. - """ - - def __init__(self, key_bindings: KeyBindingsBase) -> None: - self._bindings = key_bindings - - self.before_key_press = Event(self) - self.after_key_press = Event(self) - - self._flush_wait_task: Optional[Task[None]] = None - - self.reset() - - def reset(self) -> None: - self._previous_key_sequence: List[KeyPress] = [] - self._previous_handler: Optional[Binding] = None - - # The queue of keys not yet send to our _process generator/state machine. - self.input_queue: Deque[KeyPress] = deque() - - # The key buffer that is matched in the generator state machine. - # (This is at at most the amount of keys that make up for one key binding.) - self.key_buffer: List[KeyPress] = [] - - #: Readline argument (for repetition of commands.) - #: https://www.gnu.org/software/bash/manual/html_node/Readline-Arguments.html - self.arg: Optional[str] = None - - # Start the processor coroutine. - self._process_coroutine = self._process() - self._process_coroutine.send(None) # type: ignore - - def _get_matches(self, key_presses: List[KeyPress]) -> List[Binding]: - """ - For a list of :class:`KeyPress` instances. Give the matching handlers - that would handle this. - """ - keys = tuple(k.key for k in key_presses) - - # Try match, with mode flag - return [b for b in self._bindings.get_bindings_for_keys(keys) if b.filter()] - - def _is_prefix_of_longer_match(self, key_presses: List[KeyPress]) -> bool: - """ - For a list of :class:`KeyPress` instances. Return True if there is any - handler that is bound to a suffix of this keys. - """ - keys = tuple(k.key for k in key_presses) - - # Get the filters for all the key bindings that have a longer match. - # Note that we transform it into a `set`, because we don't care about - # the actual bindings and executing it more than once doesn't make - # sense. (Many key bindings share the same filter.) - filters = set( - b.filter for b in self._bindings.get_bindings_starting_with_keys(keys) - ) - - # When any key binding is active, return True. - return any(f() for f in filters) - - def _process(self) -> Generator[None, KeyPress, None]: - """ - Coroutine implementing the key match algorithm. Key strokes are sent - into this generator, and it calls the appropriate handlers. - """ - buffer = self.key_buffer - retry = False - - while True: - flush = False - - if retry: - retry = False - else: - key = yield - if key is _Flush: - flush = True - else: - buffer.append(key) - - # If we have some key presses, check for matches. - if buffer: - matches = self._get_matches(buffer) - - if flush: - is_prefix_of_longer_match = False - else: - is_prefix_of_longer_match = self._is_prefix_of_longer_match(buffer) - - # When eager matches were found, give priority to them and also - # ignore all the longer matches. - eager_matches = [m for m in matches if m.eager()] - - if eager_matches: - matches = eager_matches - is_prefix_of_longer_match = False - - # Exact matches found, call handler. - if not is_prefix_of_longer_match and matches: - self._call_handler(matches[-1], key_sequence=buffer[:]) - del buffer[:] # Keep reference. - - # No match found. - elif not is_prefix_of_longer_match and not matches: - retry = True - found = False - - # Loop over the input, try longest match first and shift. - for i in range(len(buffer), 0, -1): - matches = self._get_matches(buffer[:i]) - if matches: - self._call_handler(matches[-1], key_sequence=buffer[:i]) - del buffer[:i] - found = True - break - - if not found: - del buffer[:1] - - def feed(self, key_press: KeyPress, first: bool = False) -> None: - """ - Add a new :class:`KeyPress` to the input queue. - (Don't forget to call `process_keys` in order to process the queue.) - - :param first: If true, insert before everything else. - """ - if first: - self.input_queue.appendleft(key_press) - else: - self.input_queue.append(key_press) - - def feed_multiple(self, key_presses: List[KeyPress], first: bool = False) -> None: - """ - :param first: If true, insert before everything else. - """ - if first: - self.input_queue.extendleft(reversed(key_presses)) - else: - self.input_queue.extend(key_presses) - - def process_keys(self) -> None: - """ - Process all the keys in the `input_queue`. - (To be called after `feed`.) - - Note: because of the `feed`/`process_keys` separation, it is - possible to call `feed` from inside a key binding. - This function keeps looping until the queue is empty. - """ - app = get_app() - - def not_empty() -> bool: - # When the application result is set, stop processing keys. (E.g. - # if ENTER was received, followed by a few additional key strokes, - # leave the other keys in the queue.) - if app.is_done: - # But if there are still CPRResponse keys in the queue, these - # need to be processed. - return any(k for k in self.input_queue if k.key == Keys.CPRResponse) - else: - return bool(self.input_queue) - - def get_next() -> KeyPress: - if app.is_done: - # Only process CPR responses. Everything else is typeahead. - cpr = [k for k in self.input_queue if k.key == Keys.CPRResponse][0] - self.input_queue.remove(cpr) - return cpr - else: - return self.input_queue.popleft() - - is_flush = False - - while not_empty(): - # Process next key. - key_press = get_next() - - is_flush = key_press is _Flush - is_cpr = key_press.key == Keys.CPRResponse - - if not is_flush and not is_cpr: - self.before_key_press.fire() - - try: - self._process_coroutine.send(key_press) - except Exception: - # If for some reason something goes wrong in the parser, (maybe - # an exception was raised) restart the processor for next time. - self.reset() - self.empty_queue() - raise - - if not is_flush and not is_cpr: - self.after_key_press.fire() - - # Skip timeout if the last key was flush. - if not is_flush: - self._start_timeout() - - def empty_queue(self) -> List[KeyPress]: - """ - Empty the input queue. Return the unprocessed input. - """ - key_presses = list(self.input_queue) - self.input_queue.clear() - - # Filter out CPRs. We don't want to return these. - key_presses = [k for k in key_presses if k.key != Keys.CPRResponse] - return key_presses - - def _call_handler(self, handler: Binding, key_sequence: List[KeyPress]) -> None: - app = get_app() - was_recording_emacs = app.emacs_state.is_recording - was_recording_vi = bool(app.vi_state.recording_register) - was_temporary_navigation_mode = app.vi_state.temporary_navigation_mode - arg = self.arg - self.arg = None - - event = KeyPressEvent( - weakref.ref(self), - arg=arg, - key_sequence=key_sequence, - previous_key_sequence=self._previous_key_sequence, - is_repeat=(handler == self._previous_handler), - ) - - # Save the state of the current buffer. - if handler.save_before(event): - event.app.current_buffer.save_to_undo_stack() - - # Call handler. - from prompt_toolkit.buffer import EditReadOnlyBuffer - - try: - handler.call(event) - self._fix_vi_cursor_position(event) - - except EditReadOnlyBuffer: - # When a key binding does an attempt to change a buffer which is - # read-only, we can ignore that. We sound a bell and go on. - app.output.bell() - - if was_temporary_navigation_mode: - self._leave_vi_temp_navigation_mode(event) - - self._previous_key_sequence = key_sequence - self._previous_handler = handler - - # Record the key sequence in our macro. (Only if we're in macro mode - # before and after executing the key.) - if handler.record_in_macro(): - if app.emacs_state.is_recording and was_recording_emacs: - recording = app.emacs_state.current_recording - if recording is not None: # Should always be true, given that - # `was_recording_emacs` is set. - recording.extend(key_sequence) - - if app.vi_state.recording_register and was_recording_vi: - for k in key_sequence: - app.vi_state.current_recording += k.data - - def _fix_vi_cursor_position(self, event: "KeyPressEvent") -> None: - """ - After every command, make sure that if we are in Vi navigation mode, we - never put the cursor after the last character of a line. (Unless it's - an empty line.) - """ - app = event.app - buff = app.current_buffer - preferred_column = buff.preferred_column - - if ( - vi_navigation_mode() - and buff.document.is_cursor_at_the_end_of_line - and len(buff.document.current_line) > 0 - ): - buff.cursor_position -= 1 - - # Set the preferred_column for arrow up/down again. - # (This was cleared after changing the cursor position.) - buff.preferred_column = preferred_column - - def _leave_vi_temp_navigation_mode(self, event: "KeyPressEvent") -> None: - """ - If we're in Vi temporary navigation (normal) mode, return to - insert/replace mode after executing one action. - """ - app = event.app - - if app.editing_mode == EditingMode.VI: - # Not waiting for a text object and no argument has been given. - if app.vi_state.operator_func is None and self.arg is None: - app.vi_state.temporary_navigation_mode = False - - def _start_timeout(self) -> None: - """ - Start auto flush timeout. Similar to Vim's `timeoutlen` option. - - Start a background coroutine with a timer. When this timeout expires - and no key was pressed in the meantime, we flush all data in the queue - and call the appropriate key binding handlers. - """ - app = get_app() - timeout = app.timeoutlen - - if timeout is None: - return - - async def wait() -> None: - "Wait for timeout." - # This sleep can be cancelled. In that case we don't flush. - await sleep(timeout) - - if len(self.key_buffer) > 0: - # (No keys pressed in the meantime.) - flush_keys() - - def flush_keys() -> None: - "Flush keys." - self.feed(_Flush) - self.process_keys() - - # Automatically flush keys. - if self._flush_wait_task: - self._flush_wait_task.cancel() - self._flush_wait_task = app.create_background_task(wait()) - +# *** encoding: utf-8 *** +""" +An :class:`~.KeyProcessor` receives callbacks for the keystrokes parsed from +the input in the :class:`~prompt_toolkit.inputstream.InputStream` instance. + +The `KeyProcessor` will according to the implemented keybindings call the +correct callbacks when new key presses are feed through `feed`. +""" +import weakref +from asyncio import Task, sleep +from collections import deque +from typing import TYPE_CHECKING, Any, Deque, Generator, List, Optional, Union + +from prompt_toolkit.application.current import get_app +from prompt_toolkit.enums import EditingMode +from prompt_toolkit.filters.app import vi_navigation_mode +from prompt_toolkit.keys import Keys +from prompt_toolkit.utils import Event + +from .key_bindings import Binding, KeyBindingsBase + +if TYPE_CHECKING: + from prompt_toolkit.application import Application + from prompt_toolkit.buffer import Buffer + + +__all__ = [ + "KeyProcessor", + "KeyPress", + "KeyPressEvent", +] + + +class KeyPress: + """ + :param key: A `Keys` instance or text (one character). + :param data: The received string on stdin. (Often vt100 escape codes.) + """ + + def __init__(self, key: Union[Keys, str], data: Optional[str] = None) -> None: + assert isinstance(key, Keys) or len(key) == 1 + + if data is None: + if isinstance(key, Keys): + data = key.value + else: + data = key # 'key' is a one character string. + + self.key = key + self.data = data + + def __repr__(self) -> str: + return "%s(key=%r, data=%r)" % (self.__class__.__name__, self.key, self.data) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, KeyPress): + return False + return self.key == other.key and self.data == other.data + + +""" +Helper object to indicate flush operation in the KeyProcessor. +NOTE: the implementation is very similar to the VT100 parser. +""" +_Flush = KeyPress("?", data="_Flush") + + +class KeyProcessor: + """ + Statemachine that receives :class:`KeyPress` instances and according to the + key bindings in the given :class:`KeyBindings`, calls the matching handlers. + + :: + + p = KeyProcessor(key_bindings) + + # Send keys into the processor. + p.feed(KeyPress(Keys.ControlX, '\x18')) + p.feed(KeyPress(Keys.ControlC, '\x03') + + # Process all the keys in the queue. + p.process_keys() + + # Now the ControlX-ControlC callback will be called if this sequence is + # registered in the key bindings. + + :param key_bindings: `KeyBindingsBase` instance. + """ + + def __init__(self, key_bindings: KeyBindingsBase) -> None: + self._bindings = key_bindings + + self.before_key_press = Event(self) + self.after_key_press = Event(self) + + self._flush_wait_task: Optional[Task[None]] = None + + self.reset() + + def reset(self) -> None: + self._previous_key_sequence: List[KeyPress] = [] + self._previous_handler: Optional[Binding] = None + + # The queue of keys not yet send to our _process generator/state machine. + self.input_queue: Deque[KeyPress] = deque() + + # The key buffer that is matched in the generator state machine. + # (This is at at most the amount of keys that make up for one key binding.) + self.key_buffer: List[KeyPress] = [] + + #: Readline argument (for repetition of commands.) + #: https://www.gnu.org/software/bash/manual/html_node/Readline-Arguments.html + self.arg: Optional[str] = None + + # Start the processor coroutine. + self._process_coroutine = self._process() + self._process_coroutine.send(None) # type: ignore + + def _get_matches(self, key_presses: List[KeyPress]) -> List[Binding]: + """ + For a list of :class:`KeyPress` instances. Give the matching handlers + that would handle this. + """ + keys = tuple(k.key for k in key_presses) + + # Try match, with mode flag + return [b for b in self._bindings.get_bindings_for_keys(keys) if b.filter()] + + def _is_prefix_of_longer_match(self, key_presses: List[KeyPress]) -> bool: + """ + For a list of :class:`KeyPress` instances. Return True if there is any + handler that is bound to a suffix of this keys. + """ + keys = tuple(k.key for k in key_presses) + + # Get the filters for all the key bindings that have a longer match. + # Note that we transform it into a `set`, because we don't care about + # the actual bindings and executing it more than once doesn't make + # sense. (Many key bindings share the same filter.) + filters = set( + b.filter for b in self._bindings.get_bindings_starting_with_keys(keys) + ) + + # When any key binding is active, return True. + return any(f() for f in filters) + + def _process(self) -> Generator[None, KeyPress, None]: + """ + Coroutine implementing the key match algorithm. Key strokes are sent + into this generator, and it calls the appropriate handlers. + """ + buffer = self.key_buffer + retry = False + + while True: + flush = False + + if retry: + retry = False + else: + key = yield + if key is _Flush: + flush = True + else: + buffer.append(key) + + # If we have some key presses, check for matches. + if buffer: + matches = self._get_matches(buffer) + + if flush: + is_prefix_of_longer_match = False + else: + is_prefix_of_longer_match = self._is_prefix_of_longer_match(buffer) + + # When eager matches were found, give priority to them and also + # ignore all the longer matches. + eager_matches = [m for m in matches if m.eager()] + + if eager_matches: + matches = eager_matches + is_prefix_of_longer_match = False + + # Exact matches found, call handler. + if not is_prefix_of_longer_match and matches: + self._call_handler(matches[-1], key_sequence=buffer[:]) + del buffer[:] # Keep reference. + + # No match found. + elif not is_prefix_of_longer_match and not matches: + retry = True + found = False + + # Loop over the input, try longest match first and shift. + for i in range(len(buffer), 0, -1): + matches = self._get_matches(buffer[:i]) + if matches: + self._call_handler(matches[-1], key_sequence=buffer[:i]) + del buffer[:i] + found = True + break + + if not found: + del buffer[:1] + + def feed(self, key_press: KeyPress, first: bool = False) -> None: + """ + Add a new :class:`KeyPress` to the input queue. + (Don't forget to call `process_keys` in order to process the queue.) + + :param first: If true, insert before everything else. + """ + if first: + self.input_queue.appendleft(key_press) + else: + self.input_queue.append(key_press) + + def feed_multiple(self, key_presses: List[KeyPress], first: bool = False) -> None: + """ + :param first: If true, insert before everything else. + """ + if first: + self.input_queue.extendleft(reversed(key_presses)) + else: + self.input_queue.extend(key_presses) + + def process_keys(self) -> None: + """ + Process all the keys in the `input_queue`. + (To be called after `feed`.) + + Note: because of the `feed`/`process_keys` separation, it is + possible to call `feed` from inside a key binding. + This function keeps looping until the queue is empty. + """ + app = get_app() + + def not_empty() -> bool: + # When the application result is set, stop processing keys. (E.g. + # if ENTER was received, followed by a few additional key strokes, + # leave the other keys in the queue.) + if app.is_done: + # But if there are still CPRResponse keys in the queue, these + # need to be processed. + return any(k for k in self.input_queue if k.key == Keys.CPRResponse) + else: + return bool(self.input_queue) + + def get_next() -> KeyPress: + if app.is_done: + # Only process CPR responses. Everything else is typeahead. + cpr = [k for k in self.input_queue if k.key == Keys.CPRResponse][0] + self.input_queue.remove(cpr) + return cpr + else: + return self.input_queue.popleft() + + is_flush = False + + while not_empty(): + # Process next key. + key_press = get_next() + + is_flush = key_press is _Flush + is_cpr = key_press.key == Keys.CPRResponse + + if not is_flush and not is_cpr: + self.before_key_press.fire() + + try: + self._process_coroutine.send(key_press) + except Exception: + # If for some reason something goes wrong in the parser, (maybe + # an exception was raised) restart the processor for next time. + self.reset() + self.empty_queue() + raise + + if not is_flush and not is_cpr: + self.after_key_press.fire() + + # Skip timeout if the last key was flush. + if not is_flush: + self._start_timeout() + + def empty_queue(self) -> List[KeyPress]: + """ + Empty the input queue. Return the unprocessed input. + """ + key_presses = list(self.input_queue) + self.input_queue.clear() + + # Filter out CPRs. We don't want to return these. + key_presses = [k for k in key_presses if k.key != Keys.CPRResponse] + return key_presses + + def _call_handler(self, handler: Binding, key_sequence: List[KeyPress]) -> None: + app = get_app() + was_recording_emacs = app.emacs_state.is_recording + was_recording_vi = bool(app.vi_state.recording_register) + was_temporary_navigation_mode = app.vi_state.temporary_navigation_mode + arg = self.arg + self.arg = None + + event = KeyPressEvent( + weakref.ref(self), + arg=arg, + key_sequence=key_sequence, + previous_key_sequence=self._previous_key_sequence, + is_repeat=(handler == self._previous_handler), + ) + + # Save the state of the current buffer. + if handler.save_before(event): + event.app.current_buffer.save_to_undo_stack() + + # Call handler. + from prompt_toolkit.buffer import EditReadOnlyBuffer + + try: + handler.call(event) + self._fix_vi_cursor_position(event) + + except EditReadOnlyBuffer: + # When a key binding does an attempt to change a buffer which is + # read-only, we can ignore that. We sound a bell and go on. + app.output.bell() + + if was_temporary_navigation_mode: + self._leave_vi_temp_navigation_mode(event) + + self._previous_key_sequence = key_sequence + self._previous_handler = handler + + # Record the key sequence in our macro. (Only if we're in macro mode + # before and after executing the key.) + if handler.record_in_macro(): + if app.emacs_state.is_recording and was_recording_emacs: + recording = app.emacs_state.current_recording + if recording is not None: # Should always be true, given that + # `was_recording_emacs` is set. + recording.extend(key_sequence) + + if app.vi_state.recording_register and was_recording_vi: + for k in key_sequence: + app.vi_state.current_recording += k.data + + def _fix_vi_cursor_position(self, event: "KeyPressEvent") -> None: + """ + After every command, make sure that if we are in Vi navigation mode, we + never put the cursor after the last character of a line. (Unless it's + an empty line.) + """ + app = event.app + buff = app.current_buffer + preferred_column = buff.preferred_column + + if ( + vi_navigation_mode() + and buff.document.is_cursor_at_the_end_of_line + and len(buff.document.current_line) > 0 + ): + buff.cursor_position -= 1 + + # Set the preferred_column for arrow up/down again. + # (This was cleared after changing the cursor position.) + buff.preferred_column = preferred_column + + def _leave_vi_temp_navigation_mode(self, event: "KeyPressEvent") -> None: + """ + If we're in Vi temporary navigation (normal) mode, return to + insert/replace mode after executing one action. + """ + app = event.app + + if app.editing_mode == EditingMode.VI: + # Not waiting for a text object and no argument has been given. + if app.vi_state.operator_func is None and self.arg is None: + app.vi_state.temporary_navigation_mode = False + + def _start_timeout(self) -> None: + """ + Start auto flush timeout. Similar to Vim's `timeoutlen` option. + + Start a background coroutine with a timer. When this timeout expires + and no key was pressed in the meantime, we flush all data in the queue + and call the appropriate key binding handlers. + """ + app = get_app() + timeout = app.timeoutlen + + if timeout is None: + return + + async def wait() -> None: + "Wait for timeout." + # This sleep can be cancelled. In that case we don't flush. + await sleep(timeout) + + if len(self.key_buffer) > 0: + # (No keys pressed in the meantime.) + flush_keys() + + def flush_keys() -> None: + "Flush keys." + self.feed(_Flush) + self.process_keys() + + # Automatically flush keys. + if self._flush_wait_task: + self._flush_wait_task.cancel() + self._flush_wait_task = app.create_background_task(wait()) + def send_sigint(self) -> None: """ Send SIGINT. Immediately call the SIGINT key handler. """ self.feed(KeyPress(key=Keys.SIGINT), first=True) self.process_keys() - - -class KeyPressEvent: - """ - Key press event, delivered to key bindings. - - :param key_processor_ref: Weak reference to the `KeyProcessor`. - :param arg: Repetition argument. - :param key_sequence: List of `KeyPress` instances. - :param previouskey_sequence: Previous list of `KeyPress` instances. - :param is_repeat: True when the previous event was delivered to the same handler. - """ - - def __init__( - self, - key_processor_ref: "weakref.ReferenceType[KeyProcessor]", - arg: Optional[str], - key_sequence: List[KeyPress], - previous_key_sequence: List[KeyPress], - is_repeat: bool, - ) -> None: - - self._key_processor_ref = key_processor_ref - self.key_sequence = key_sequence - self.previous_key_sequence = previous_key_sequence - - #: True when the previous key sequence was handled by the same handler. - self.is_repeat = is_repeat - - self._arg = arg - self._app = get_app() - - def __repr__(self) -> str: - return "KeyPressEvent(arg=%r, key_sequence=%r, is_repeat=%r)" % ( - self.arg, - self.key_sequence, - self.is_repeat, - ) - - @property - def data(self) -> str: - return self.key_sequence[-1].data - - @property - def key_processor(self) -> KeyProcessor: - processor = self._key_processor_ref() - if processor is None: - raise Exception("KeyProcessor was lost. This should not happen.") - return processor - - @property - def app(self) -> "Application[Any]": - """ - The current `Application` object. - """ - return self._app - - @property - def current_buffer(self) -> "Buffer": - """ - The current buffer. - """ - return self.app.current_buffer - - @property - def arg(self) -> int: - """ - Repetition argument. - """ - if self._arg == "-": - return -1 - - result = int(self._arg or 1) - - # Don't exceed a million. - if int(result) >= 1000000: - result = 1 - - return result - - @property - def arg_present(self) -> bool: - """ - True if repetition argument was explicitly provided. - """ - return self._arg is not None - - def append_to_arg_count(self, data: str) -> None: - """ - Add digit to the input argument. - - :param data: the typed digit as string - """ - assert data in "-0123456789" - current = self._arg - - if data == "-": - assert current is None or current == "-" - result = data - elif current is None: - result = data - else: - result = "%s%s" % (current, data) - - self.key_processor.arg = result - - @property - def cli(self) -> "Application[Any]": - "For backward-compatibility." - return self.app + + +class KeyPressEvent: + """ + Key press event, delivered to key bindings. + + :param key_processor_ref: Weak reference to the `KeyProcessor`. + :param arg: Repetition argument. + :param key_sequence: List of `KeyPress` instances. + :param previouskey_sequence: Previous list of `KeyPress` instances. + :param is_repeat: True when the previous event was delivered to the same handler. + """ + + def __init__( + self, + key_processor_ref: "weakref.ReferenceType[KeyProcessor]", + arg: Optional[str], + key_sequence: List[KeyPress], + previous_key_sequence: List[KeyPress], + is_repeat: bool, + ) -> None: + + self._key_processor_ref = key_processor_ref + self.key_sequence = key_sequence + self.previous_key_sequence = previous_key_sequence + + #: True when the previous key sequence was handled by the same handler. + self.is_repeat = is_repeat + + self._arg = arg + self._app = get_app() + + def __repr__(self) -> str: + return "KeyPressEvent(arg=%r, key_sequence=%r, is_repeat=%r)" % ( + self.arg, + self.key_sequence, + self.is_repeat, + ) + + @property + def data(self) -> str: + return self.key_sequence[-1].data + + @property + def key_processor(self) -> KeyProcessor: + processor = self._key_processor_ref() + if processor is None: + raise Exception("KeyProcessor was lost. This should not happen.") + return processor + + @property + def app(self) -> "Application[Any]": + """ + The current `Application` object. + """ + return self._app + + @property + def current_buffer(self) -> "Buffer": + """ + The current buffer. + """ + return self.app.current_buffer + + @property + def arg(self) -> int: + """ + Repetition argument. + """ + if self._arg == "-": + return -1 + + result = int(self._arg or 1) + + # Don't exceed a million. + if int(result) >= 1000000: + result = 1 + + return result + + @property + def arg_present(self) -> bool: + """ + True if repetition argument was explicitly provided. + """ + return self._arg is not None + + def append_to_arg_count(self, data: str) -> None: + """ + Add digit to the input argument. + + :param data: the typed digit as string + """ + assert data in "-0123456789" + current = self._arg + + if data == "-": + assert current is None or current == "-" + result = data + elif current is None: + result = data + else: + result = "%s%s" % (current, data) + + self.key_processor.arg = result + + @property + def cli(self) -> "Application[Any]": + "For backward-compatibility." + return self.app diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/vi_state.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/vi_state.py index b510655618..10593a82e6 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/vi_state.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/vi_state.py @@ -1,107 +1,107 @@ -from enum import Enum -from typing import TYPE_CHECKING, Callable, Dict, Optional - -from prompt_toolkit.clipboard import ClipboardData - -if TYPE_CHECKING: - from .key_bindings.vi import TextObject - from .key_processor import KeyPressEvent - -__all__ = [ - "InputMode", - "CharacterFind", - "ViState", -] - - -class InputMode(str, Enum): - value: str - - INSERT = "vi-insert" - INSERT_MULTIPLE = "vi-insert-multiple" - NAVIGATION = "vi-navigation" # Normal mode. - REPLACE = "vi-replace" - REPLACE_SINGLE = "vi-replace-single" - - -class CharacterFind: - def __init__(self, character: str, backwards: bool = False) -> None: - self.character = character - self.backwards = backwards - - -class ViState: - """ - Mutable class to hold the state of the Vi navigation. - """ - - def __init__(self) -> None: - #: None or CharacterFind instance. (This is used to repeat the last - #: search in Vi mode, by pressing the 'n' or 'N' in navigation mode.) - self.last_character_find: Optional[CharacterFind] = None - - # When an operator is given and we are waiting for text object, - # -- e.g. in the case of 'dw', after the 'd' --, an operator callback - # is set here. - self.operator_func: Optional[ - Callable[["KeyPressEvent", "TextObject"], None] - ] = None - self.operator_arg: Optional[int] = None - - #: Named registers. Maps register name (e.g. 'a') to - #: :class:`ClipboardData` instances. - self.named_registers: Dict[str, ClipboardData] = {} - - #: The Vi mode we're currently in to. - self.__input_mode = InputMode.INSERT - - #: Waiting for digraph. - self.waiting_for_digraph = False - self.digraph_symbol1: Optional[str] = None # (None or a symbol.) - - #: When true, make ~ act as an operator. - self.tilde_operator = False - - #: Register in which we are recording a macro. - #: `None` when not recording anything. - # Note that the recording is only stored in the register after the - # recording is stopped. So we record in a separate `current_recording` - # variable. - self.recording_register: Optional[str] = None - self.current_recording: str = "" - - # Temporary navigation (normal) mode. - # This happens when control-o has been pressed in insert or replace - # mode. The user can now do one navigation action and we'll return back - # to insert/replace. - self.temporary_navigation_mode = False - - @property - def input_mode(self) -> InputMode: - "Get `InputMode`." - return self.__input_mode - - @input_mode.setter - def input_mode(self, value: InputMode) -> None: - "Set `InputMode`." - if value == InputMode.NAVIGATION: - self.waiting_for_digraph = False - self.operator_func = None - self.operator_arg = None - - self.__input_mode = value - - def reset(self) -> None: - """ - Reset state, go back to the given mode. INSERT by default. - """ - # Go back to insert mode. - self.input_mode = InputMode.INSERT - - self.waiting_for_digraph = False - self.operator_func = None - self.operator_arg = None - - # Reset recording state. - self.recording_register = None - self.current_recording = "" +from enum import Enum +from typing import TYPE_CHECKING, Callable, Dict, Optional + +from prompt_toolkit.clipboard import ClipboardData + +if TYPE_CHECKING: + from .key_bindings.vi import TextObject + from .key_processor import KeyPressEvent + +__all__ = [ + "InputMode", + "CharacterFind", + "ViState", +] + + +class InputMode(str, Enum): + value: str + + INSERT = "vi-insert" + INSERT_MULTIPLE = "vi-insert-multiple" + NAVIGATION = "vi-navigation" # Normal mode. + REPLACE = "vi-replace" + REPLACE_SINGLE = "vi-replace-single" + + +class CharacterFind: + def __init__(self, character: str, backwards: bool = False) -> None: + self.character = character + self.backwards = backwards + + +class ViState: + """ + Mutable class to hold the state of the Vi navigation. + """ + + def __init__(self) -> None: + #: None or CharacterFind instance. (This is used to repeat the last + #: search in Vi mode, by pressing the 'n' or 'N' in navigation mode.) + self.last_character_find: Optional[CharacterFind] = None + + # When an operator is given and we are waiting for text object, + # -- e.g. in the case of 'dw', after the 'd' --, an operator callback + # is set here. + self.operator_func: Optional[ + Callable[["KeyPressEvent", "TextObject"], None] + ] = None + self.operator_arg: Optional[int] = None + + #: Named registers. Maps register name (e.g. 'a') to + #: :class:`ClipboardData` instances. + self.named_registers: Dict[str, ClipboardData] = {} + + #: The Vi mode we're currently in to. + self.__input_mode = InputMode.INSERT + + #: Waiting for digraph. + self.waiting_for_digraph = False + self.digraph_symbol1: Optional[str] = None # (None or a symbol.) + + #: When true, make ~ act as an operator. + self.tilde_operator = False + + #: Register in which we are recording a macro. + #: `None` when not recording anything. + # Note that the recording is only stored in the register after the + # recording is stopped. So we record in a separate `current_recording` + # variable. + self.recording_register: Optional[str] = None + self.current_recording: str = "" + + # Temporary navigation (normal) mode. + # This happens when control-o has been pressed in insert or replace + # mode. The user can now do one navigation action and we'll return back + # to insert/replace. + self.temporary_navigation_mode = False + + @property + def input_mode(self) -> InputMode: + "Get `InputMode`." + return self.__input_mode + + @input_mode.setter + def input_mode(self, value: InputMode) -> None: + "Set `InputMode`." + if value == InputMode.NAVIGATION: + self.waiting_for_digraph = False + self.operator_func = None + self.operator_arg = None + + self.__input_mode = value + + def reset(self) -> None: + """ + Reset state, go back to the given mode. INSERT by default. + """ + # Go back to insert mode. + self.input_mode = InputMode.INSERT + + self.waiting_for_digraph = False + self.operator_func = None + self.operator_arg = None + + # Reset recording state. + self.recording_register = None + self.current_recording = "" diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/keys.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/keys.py index 4ebcfd5d30..e10ba9d921 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/keys.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/keys.py @@ -1,221 +1,221 @@ -from enum import Enum -from typing import Dict, List - -__all__ = [ - "Keys", - "ALL_KEYS", -] - - -class Keys(str, Enum): - """ - List of keys for use in key bindings. - - Note that this is an "StrEnum", all values can be compared against - strings. - """ - - value: str - - Escape = "escape" # Also Control-[ - ShiftEscape = "s-escape" - - ControlAt = "c-@" # Also Control-Space. - - ControlA = "c-a" - ControlB = "c-b" - ControlC = "c-c" - ControlD = "c-d" - ControlE = "c-e" - ControlF = "c-f" - ControlG = "c-g" - ControlH = "c-h" - ControlI = "c-i" # Tab - ControlJ = "c-j" # Newline - ControlK = "c-k" - ControlL = "c-l" - ControlM = "c-m" # Carriage return - ControlN = "c-n" - ControlO = "c-o" - ControlP = "c-p" - ControlQ = "c-q" - ControlR = "c-r" - ControlS = "c-s" - ControlT = "c-t" - ControlU = "c-u" - ControlV = "c-v" - ControlW = "c-w" - ControlX = "c-x" - ControlY = "c-y" - ControlZ = "c-z" - - Control1 = "c-1" - Control2 = "c-2" - Control3 = "c-3" - Control4 = "c-4" - Control5 = "c-5" - Control6 = "c-6" - Control7 = "c-7" - Control8 = "c-8" - Control9 = "c-9" - Control0 = "c-0" - - ControlShift1 = "c-s-1" - ControlShift2 = "c-s-2" - ControlShift3 = "c-s-3" - ControlShift4 = "c-s-4" - ControlShift5 = "c-s-5" - ControlShift6 = "c-s-6" - ControlShift7 = "c-s-7" - ControlShift8 = "c-s-8" - ControlShift9 = "c-s-9" - ControlShift0 = "c-s-0" - - ControlBackslash = "c-\\" - ControlSquareClose = "c-]" - ControlCircumflex = "c-^" - ControlUnderscore = "c-_" - - Left = "left" - Right = "right" - Up = "up" - Down = "down" - Home = "home" - End = "end" - Insert = "insert" - Delete = "delete" - PageUp = "pageup" - PageDown = "pagedown" - - ControlLeft = "c-left" - ControlRight = "c-right" - ControlUp = "c-up" - ControlDown = "c-down" - ControlHome = "c-home" - ControlEnd = "c-end" - ControlInsert = "c-insert" - ControlDelete = "c-delete" - ControlPageUp = "c-pageup" - ControlPageDown = "c-pagedown" - - ShiftLeft = "s-left" - ShiftRight = "s-right" - ShiftUp = "s-up" - ShiftDown = "s-down" - ShiftHome = "s-home" - ShiftEnd = "s-end" - ShiftInsert = "s-insert" - ShiftDelete = "s-delete" - ShiftPageUp = "s-pageup" - ShiftPageDown = "s-pagedown" - - ControlShiftLeft = "c-s-left" - ControlShiftRight = "c-s-right" - ControlShiftUp = "c-s-up" - ControlShiftDown = "c-s-down" - ControlShiftHome = "c-s-home" - ControlShiftEnd = "c-s-end" - ControlShiftInsert = "c-s-insert" - ControlShiftDelete = "c-s-delete" - ControlShiftPageUp = "c-s-pageup" - ControlShiftPageDown = "c-s-pagedown" - - BackTab = "s-tab" # shift + tab - - F1 = "f1" - F2 = "f2" - F3 = "f3" - F4 = "f4" - F5 = "f5" - F6 = "f6" - F7 = "f7" - F8 = "f8" - F9 = "f9" - F10 = "f10" - F11 = "f11" - F12 = "f12" - F13 = "f13" - F14 = "f14" - F15 = "f15" - F16 = "f16" - F17 = "f17" - F18 = "f18" - F19 = "f19" - F20 = "f20" - F21 = "f21" - F22 = "f22" - F23 = "f23" - F24 = "f24" - - ControlF1 = "c-f1" - ControlF2 = "c-f2" - ControlF3 = "c-f3" - ControlF4 = "c-f4" - ControlF5 = "c-f5" - ControlF6 = "c-f6" - ControlF7 = "c-f7" - ControlF8 = "c-f8" - ControlF9 = "c-f9" - ControlF10 = "c-f10" - ControlF11 = "c-f11" - ControlF12 = "c-f12" - ControlF13 = "c-f13" - ControlF14 = "c-f14" - ControlF15 = "c-f15" - ControlF16 = "c-f16" - ControlF17 = "c-f17" - ControlF18 = "c-f18" - ControlF19 = "c-f19" - ControlF20 = "c-f20" - ControlF21 = "c-f21" - ControlF22 = "c-f22" - ControlF23 = "c-f23" - ControlF24 = "c-f24" - - # Matches any key. - Any = "<any>" - - # Special. - ScrollUp = "<scroll-up>" - ScrollDown = "<scroll-down>" - - CPRResponse = "<cursor-position-response>" - Vt100MouseEvent = "<vt100-mouse-event>" - WindowsMouseEvent = "<windows-mouse-event>" - BracketedPaste = "<bracketed-paste>" - +from enum import Enum +from typing import Dict, List + +__all__ = [ + "Keys", + "ALL_KEYS", +] + + +class Keys(str, Enum): + """ + List of keys for use in key bindings. + + Note that this is an "StrEnum", all values can be compared against + strings. + """ + + value: str + + Escape = "escape" # Also Control-[ + ShiftEscape = "s-escape" + + ControlAt = "c-@" # Also Control-Space. + + ControlA = "c-a" + ControlB = "c-b" + ControlC = "c-c" + ControlD = "c-d" + ControlE = "c-e" + ControlF = "c-f" + ControlG = "c-g" + ControlH = "c-h" + ControlI = "c-i" # Tab + ControlJ = "c-j" # Newline + ControlK = "c-k" + ControlL = "c-l" + ControlM = "c-m" # Carriage return + ControlN = "c-n" + ControlO = "c-o" + ControlP = "c-p" + ControlQ = "c-q" + ControlR = "c-r" + ControlS = "c-s" + ControlT = "c-t" + ControlU = "c-u" + ControlV = "c-v" + ControlW = "c-w" + ControlX = "c-x" + ControlY = "c-y" + ControlZ = "c-z" + + Control1 = "c-1" + Control2 = "c-2" + Control3 = "c-3" + Control4 = "c-4" + Control5 = "c-5" + Control6 = "c-6" + Control7 = "c-7" + Control8 = "c-8" + Control9 = "c-9" + Control0 = "c-0" + + ControlShift1 = "c-s-1" + ControlShift2 = "c-s-2" + ControlShift3 = "c-s-3" + ControlShift4 = "c-s-4" + ControlShift5 = "c-s-5" + ControlShift6 = "c-s-6" + ControlShift7 = "c-s-7" + ControlShift8 = "c-s-8" + ControlShift9 = "c-s-9" + ControlShift0 = "c-s-0" + + ControlBackslash = "c-\\" + ControlSquareClose = "c-]" + ControlCircumflex = "c-^" + ControlUnderscore = "c-_" + + Left = "left" + Right = "right" + Up = "up" + Down = "down" + Home = "home" + End = "end" + Insert = "insert" + Delete = "delete" + PageUp = "pageup" + PageDown = "pagedown" + + ControlLeft = "c-left" + ControlRight = "c-right" + ControlUp = "c-up" + ControlDown = "c-down" + ControlHome = "c-home" + ControlEnd = "c-end" + ControlInsert = "c-insert" + ControlDelete = "c-delete" + ControlPageUp = "c-pageup" + ControlPageDown = "c-pagedown" + + ShiftLeft = "s-left" + ShiftRight = "s-right" + ShiftUp = "s-up" + ShiftDown = "s-down" + ShiftHome = "s-home" + ShiftEnd = "s-end" + ShiftInsert = "s-insert" + ShiftDelete = "s-delete" + ShiftPageUp = "s-pageup" + ShiftPageDown = "s-pagedown" + + ControlShiftLeft = "c-s-left" + ControlShiftRight = "c-s-right" + ControlShiftUp = "c-s-up" + ControlShiftDown = "c-s-down" + ControlShiftHome = "c-s-home" + ControlShiftEnd = "c-s-end" + ControlShiftInsert = "c-s-insert" + ControlShiftDelete = "c-s-delete" + ControlShiftPageUp = "c-s-pageup" + ControlShiftPageDown = "c-s-pagedown" + + BackTab = "s-tab" # shift + tab + + F1 = "f1" + F2 = "f2" + F3 = "f3" + F4 = "f4" + F5 = "f5" + F6 = "f6" + F7 = "f7" + F8 = "f8" + F9 = "f9" + F10 = "f10" + F11 = "f11" + F12 = "f12" + F13 = "f13" + F14 = "f14" + F15 = "f15" + F16 = "f16" + F17 = "f17" + F18 = "f18" + F19 = "f19" + F20 = "f20" + F21 = "f21" + F22 = "f22" + F23 = "f23" + F24 = "f24" + + ControlF1 = "c-f1" + ControlF2 = "c-f2" + ControlF3 = "c-f3" + ControlF4 = "c-f4" + ControlF5 = "c-f5" + ControlF6 = "c-f6" + ControlF7 = "c-f7" + ControlF8 = "c-f8" + ControlF9 = "c-f9" + ControlF10 = "c-f10" + ControlF11 = "c-f11" + ControlF12 = "c-f12" + ControlF13 = "c-f13" + ControlF14 = "c-f14" + ControlF15 = "c-f15" + ControlF16 = "c-f16" + ControlF17 = "c-f17" + ControlF18 = "c-f18" + ControlF19 = "c-f19" + ControlF20 = "c-f20" + ControlF21 = "c-f21" + ControlF22 = "c-f22" + ControlF23 = "c-f23" + ControlF24 = "c-f24" + + # Matches any key. + Any = "<any>" + + # Special. + ScrollUp = "<scroll-up>" + ScrollDown = "<scroll-down>" + + CPRResponse = "<cursor-position-response>" + Vt100MouseEvent = "<vt100-mouse-event>" + WindowsMouseEvent = "<windows-mouse-event>" + BracketedPaste = "<bracketed-paste>" + SIGINT = "<sigint>" - # For internal use: key which is ignored. - # (The key binding for this key should not do anything.) - Ignore = "<ignore>" - - # Some 'Key' aliases (for backwards-compatibility). - ControlSpace = ControlAt - Tab = ControlI - Enter = ControlM - Backspace = ControlH - - # ShiftControl was renamed to ControlShift in - # 888fcb6fa4efea0de8333177e1bbc792f3ff3c24 (20 Feb 2020). - ShiftControlLeft = ControlShiftLeft - ShiftControlRight = ControlShiftRight - ShiftControlHome = ControlShiftHome - ShiftControlEnd = ControlShiftEnd - - -ALL_KEYS: List[str] = [k.value for k in Keys] - - -# Aliases. -KEY_ALIASES: Dict[str, str] = { - "backspace": "c-h", - "c-space": "c-@", - "enter": "c-m", - "tab": "c-i", - # ShiftControl was renamed to ControlShift. - "s-c-left": "c-s-left", - "s-c-right": "c-s-right", - "s-c-home": "c-s-home", - "s-c-end": "c-s-end", -} + # For internal use: key which is ignored. + # (The key binding for this key should not do anything.) + Ignore = "<ignore>" + + # Some 'Key' aliases (for backwards-compatibility). + ControlSpace = ControlAt + Tab = ControlI + Enter = ControlM + Backspace = ControlH + + # ShiftControl was renamed to ControlShift in + # 888fcb6fa4efea0de8333177e1bbc792f3ff3c24 (20 Feb 2020). + ShiftControlLeft = ControlShiftLeft + ShiftControlRight = ControlShiftRight + ShiftControlHome = ControlShiftHome + ShiftControlEnd = ControlShiftEnd + + +ALL_KEYS: List[str] = [k.value for k in Keys] + + +# Aliases. +KEY_ALIASES: Dict[str, str] = { + "backspace": "c-h", + "c-space": "c-@", + "enter": "c-m", + "tab": "c-i", + # ShiftControl was renamed to ControlShift. + "s-c-left": "c-s-left", + "s-c-right": "c-s-right", + "s-c-home": "c-s-home", + "s-c-end": "c-s-end", +} diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/__init__.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/__init__.py index 348ded009b..6669da5d7a 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/__init__.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/__init__.py @@ -1,144 +1,144 @@ -""" -Command line layout definitions -------------------------------- - -The layout of a command line interface is defined by a Container instance. -There are two main groups of classes here. Containers and controls: - -- A container can contain other containers or controls, it can have multiple - children and it decides about the dimensions. -- A control is responsible for rendering the actual content to a screen. - A control can propose some dimensions, but it's the container who decides - about the dimensions -- or when the control consumes more space -- which part - of the control will be visible. - - -Container classes:: - - - Container (Abstract base class) - |- HSplit (Horizontal split) - |- VSplit (Vertical split) - |- FloatContainer (Container which can also contain menus and other floats) - `- Window (Container which contains one actual control - -Control classes:: - - - UIControl (Abstract base class) - |- FormattedTextControl (Renders formatted text, or a simple list of text fragments) - `- BufferControl (Renders an input buffer.) - - -Usually, you end up wrapping every control inside a `Window` object, because -that's the only way to render it in a layout. - -There are some prepared toolbars which are ready to use:: - -- SystemToolbar (Shows the 'system' input buffer, for entering system commands.) -- ArgToolbar (Shows the input 'arg', for repetition of input commands.) -- SearchToolbar (Shows the 'search' input buffer, for incremental search.) -- CompletionsToolbar (Shows the completions of the current buffer.) -- ValidationToolbar (Shows validation errors of the current buffer.) - -And one prepared menu: - -- CompletionsMenu - -""" -from .containers import ( - AnyContainer, - ColorColumn, - ConditionalContainer, - Container, - DynamicContainer, - Float, - FloatContainer, - HorizontalAlign, - HSplit, - ScrollOffsets, - VerticalAlign, - VSplit, - Window, - WindowAlign, - WindowRenderInfo, - is_container, - to_container, - to_window, -) -from .controls import ( - BufferControl, - DummyControl, - FormattedTextControl, - SearchBufferControl, - UIContent, - UIControl, -) -from .dimension import ( - AnyDimension, - D, - Dimension, - is_dimension, - max_layout_dimensions, - sum_layout_dimensions, - to_dimension, -) -from .layout import InvalidLayoutError, Layout, walk -from .margins import ( - ConditionalMargin, - Margin, - NumberedMargin, - PromptMargin, - ScrollbarMargin, -) -from .menus import CompletionsMenu, MultiColumnCompletionsMenu -from .scrollable_pane import ScrollablePane - -__all__ = [ - # Layout. - "Layout", - "InvalidLayoutError", - "walk", - # Dimensions. - "AnyDimension", - "Dimension", - "D", - "sum_layout_dimensions", - "max_layout_dimensions", - "to_dimension", - "is_dimension", - # Containers. - "AnyContainer", - "Container", - "HorizontalAlign", - "VerticalAlign", - "HSplit", - "VSplit", - "FloatContainer", - "Float", - "WindowAlign", - "Window", - "WindowRenderInfo", - "ConditionalContainer", - "ScrollOffsets", - "ColorColumn", - "to_container", - "to_window", - "is_container", - "DynamicContainer", - "ScrollablePane", - # Controls. - "BufferControl", - "SearchBufferControl", - "DummyControl", - "FormattedTextControl", - "UIControl", - "UIContent", - # Margins. - "Margin", - "NumberedMargin", - "ScrollbarMargin", - "ConditionalMargin", - "PromptMargin", - # Menus. - "CompletionsMenu", - "MultiColumnCompletionsMenu", -] +""" +Command line layout definitions +------------------------------- + +The layout of a command line interface is defined by a Container instance. +There are two main groups of classes here. Containers and controls: + +- A container can contain other containers or controls, it can have multiple + children and it decides about the dimensions. +- A control is responsible for rendering the actual content to a screen. + A control can propose some dimensions, but it's the container who decides + about the dimensions -- or when the control consumes more space -- which part + of the control will be visible. + + +Container classes:: + + - Container (Abstract base class) + |- HSplit (Horizontal split) + |- VSplit (Vertical split) + |- FloatContainer (Container which can also contain menus and other floats) + `- Window (Container which contains one actual control + +Control classes:: + + - UIControl (Abstract base class) + |- FormattedTextControl (Renders formatted text, or a simple list of text fragments) + `- BufferControl (Renders an input buffer.) + + +Usually, you end up wrapping every control inside a `Window` object, because +that's the only way to render it in a layout. + +There are some prepared toolbars which are ready to use:: + +- SystemToolbar (Shows the 'system' input buffer, for entering system commands.) +- ArgToolbar (Shows the input 'arg', for repetition of input commands.) +- SearchToolbar (Shows the 'search' input buffer, for incremental search.) +- CompletionsToolbar (Shows the completions of the current buffer.) +- ValidationToolbar (Shows validation errors of the current buffer.) + +And one prepared menu: + +- CompletionsMenu + +""" +from .containers import ( + AnyContainer, + ColorColumn, + ConditionalContainer, + Container, + DynamicContainer, + Float, + FloatContainer, + HorizontalAlign, + HSplit, + ScrollOffsets, + VerticalAlign, + VSplit, + Window, + WindowAlign, + WindowRenderInfo, + is_container, + to_container, + to_window, +) +from .controls import ( + BufferControl, + DummyControl, + FormattedTextControl, + SearchBufferControl, + UIContent, + UIControl, +) +from .dimension import ( + AnyDimension, + D, + Dimension, + is_dimension, + max_layout_dimensions, + sum_layout_dimensions, + to_dimension, +) +from .layout import InvalidLayoutError, Layout, walk +from .margins import ( + ConditionalMargin, + Margin, + NumberedMargin, + PromptMargin, + ScrollbarMargin, +) +from .menus import CompletionsMenu, MultiColumnCompletionsMenu +from .scrollable_pane import ScrollablePane + +__all__ = [ + # Layout. + "Layout", + "InvalidLayoutError", + "walk", + # Dimensions. + "AnyDimension", + "Dimension", + "D", + "sum_layout_dimensions", + "max_layout_dimensions", + "to_dimension", + "is_dimension", + # Containers. + "AnyContainer", + "Container", + "HorizontalAlign", + "VerticalAlign", + "HSplit", + "VSplit", + "FloatContainer", + "Float", + "WindowAlign", + "Window", + "WindowRenderInfo", + "ConditionalContainer", + "ScrollOffsets", + "ColorColumn", + "to_container", + "to_window", + "is_container", + "DynamicContainer", + "ScrollablePane", + # Controls. + "BufferControl", + "SearchBufferControl", + "DummyControl", + "FormattedTextControl", + "UIControl", + "UIContent", + # Margins. + "Margin", + "NumberedMargin", + "ScrollbarMargin", + "ConditionalMargin", + "PromptMargin", + # Menus. + "CompletionsMenu", + "MultiColumnCompletionsMenu", +] diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/containers.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/containers.py index 7059653151..2c845a76aa 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/containers.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/containers.py @@ -1,2757 +1,2757 @@ -""" -Container for the layout. -(Containers can contain other containers or user interface controls.) -""" -from abc import ABCMeta, abstractmethod -from enum import Enum -from functools import partial -from typing import ( - TYPE_CHECKING, - Callable, - Dict, - List, - Optional, - Sequence, - Tuple, - Union, - cast, -) - -from prompt_toolkit.application.current import get_app -from prompt_toolkit.cache import SimpleCache -from prompt_toolkit.data_structures import Point -from prompt_toolkit.filters import ( - FilterOrBool, - emacs_insert_mode, - to_filter, - vi_insert_mode, -) -from prompt_toolkit.formatted_text import ( - AnyFormattedText, - StyleAndTextTuples, - to_formatted_text, -) -from prompt_toolkit.formatted_text.utils import ( - fragment_list_to_text, - fragment_list_width, -) -from prompt_toolkit.key_binding import KeyBindingsBase -from prompt_toolkit.mouse_events import MouseEvent, MouseEventType -from prompt_toolkit.utils import get_cwidth, take_using_weights, to_int, to_str - -from .controls import ( - DummyControl, - FormattedTextControl, - GetLinePrefixCallable, - UIContent, - UIControl, -) -from .dimension import ( - AnyDimension, - Dimension, - max_layout_dimensions, - sum_layout_dimensions, - to_dimension, -) -from .margins import Margin -from .mouse_handlers import MouseHandlers -from .screen import _CHAR_CACHE, Screen, WritePosition -from .utils import explode_text_fragments - -if TYPE_CHECKING: - from typing_extensions import Protocol, TypeGuard - - from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone - - -__all__ = [ - "AnyContainer", - "Container", - "HorizontalAlign", - "VerticalAlign", - "HSplit", - "VSplit", - "FloatContainer", - "Float", - "WindowAlign", - "Window", - "WindowRenderInfo", - "ConditionalContainer", - "ScrollOffsets", - "ColorColumn", - "to_container", - "to_window", - "is_container", - "DynamicContainer", -] - - -class Container(metaclass=ABCMeta): - """ - Base class for user interface layout. - """ - - @abstractmethod - def reset(self) -> None: - """ - Reset the state of this container and all the children. - (E.g. reset scroll offsets, etc...) - """ - - @abstractmethod - def preferred_width(self, max_available_width: int) -> Dimension: - """ - Return a :class:`~prompt_toolkit.layout.Dimension` that represents the - desired width for this container. - """ - - @abstractmethod - def preferred_height(self, width: int, max_available_height: int) -> Dimension: - """ - Return a :class:`~prompt_toolkit.layout.Dimension` that represents the - desired height for this container. - """ - - @abstractmethod - def write_to_screen( - self, - screen: Screen, - mouse_handlers: MouseHandlers, - write_position: WritePosition, - parent_style: str, - erase_bg: bool, - z_index: Optional[int], - ) -> None: - """ - Write the actual content to the screen. - - :param screen: :class:`~prompt_toolkit.layout.screen.Screen` - :param mouse_handlers: :class:`~prompt_toolkit.layout.mouse_handlers.MouseHandlers`. - :param parent_style: Style string to pass to the :class:`.Window` - object. This will be applied to all content of the windows. - :class:`.VSplit` and :class:`.HSplit` can use it to pass their - style down to the windows that they contain. - :param z_index: Used for propagating z_index from parent to child. - """ - - def is_modal(self) -> bool: - """ - When this container is modal, key bindings from parent containers are - not taken into account if a user control in this container is focused. - """ - return False - - def get_key_bindings(self) -> Optional[KeyBindingsBase]: - """ - Returns a :class:`.KeyBindings` object. These bindings become active when any - user control in this container has the focus, except if any containers - between this container and the focused user control is modal. - """ - return None - - @abstractmethod - def get_children(self) -> List["Container"]: - """ - Return the list of child :class:`.Container` objects. - """ - return [] - - -if TYPE_CHECKING: - - class MagicContainer(Protocol): - """ - Any object that implements ``__pt_container__`` represents a container. - """ - - def __pt_container__(self) -> "AnyContainer": - ... - - -AnyContainer = Union[Container, "MagicContainer"] - - -def _window_too_small() -> "Window": - "Create a `Window` that displays the 'Window too small' text." - return Window( - FormattedTextControl(text=[("class:window-too-small", " Window too small... ")]) - ) - - -class VerticalAlign(Enum): - "Alignment for `HSplit`." - TOP = "TOP" - CENTER = "CENTER" - BOTTOM = "BOTTOM" - JUSTIFY = "JUSTIFY" - - -class HorizontalAlign(Enum): - "Alignment for `VSplit`." - LEFT = "LEFT" - CENTER = "CENTER" - RIGHT = "RIGHT" - JUSTIFY = "JUSTIFY" - - -class _Split(Container): - """ - The common parts of `VSplit` and `HSplit`. - """ - - def __init__( - self, - children: Sequence[AnyContainer], - window_too_small: Optional[Container] = None, - padding: AnyDimension = Dimension.exact(0), - padding_char: Optional[str] = None, - padding_style: str = "", - width: AnyDimension = None, - height: AnyDimension = None, - z_index: Optional[int] = None, - modal: bool = False, - key_bindings: Optional[KeyBindingsBase] = None, - style: Union[str, Callable[[], str]] = "", - ) -> None: - - self.children = [to_container(c) for c in children] - self.window_too_small = window_too_small or _window_too_small() - self.padding = padding - self.padding_char = padding_char - self.padding_style = padding_style - - self.width = width - self.height = height - self.z_index = z_index - - self.modal = modal - self.key_bindings = key_bindings - self.style = style - - def is_modal(self) -> bool: - return self.modal - - def get_key_bindings(self) -> Optional[KeyBindingsBase]: - return self.key_bindings - - def get_children(self) -> List[Container]: - return self.children - - -class HSplit(_Split): - """ - Several layouts, one stacked above/under the other. :: - - +--------------------+ - | | - +--------------------+ - | | - +--------------------+ - - By default, this doesn't display a horizontal line between the children, - but if this is something you need, then create a HSplit as follows:: - - HSplit(children=[ ... ], padding_char='-', - padding=1, padding_style='#ffff00') - - :param children: List of child :class:`.Container` objects. - :param window_too_small: A :class:`.Container` object that is displayed if - there is not enough space for all the children. By default, this is a - "Window too small" message. - :param align: `VerticalAlign` value. - :param width: When given, use this width instead of looking at the children. - :param height: When given, use this height instead of looking at the children. - :param z_index: (int or None) When specified, this can be used to bring - element in front of floating elements. `None` means: inherit from parent. - :param style: A style string. - :param modal: ``True`` or ``False``. - :param key_bindings: ``None`` or a :class:`.KeyBindings` object. - - :param padding: (`Dimension` or int), size to be used for the padding. - :param padding_char: Character to be used for filling in the padding. - :param padding_style: Style to applied to the padding. - """ - - def __init__( - self, - children: Sequence[AnyContainer], - window_too_small: Optional[Container] = None, - align: VerticalAlign = VerticalAlign.JUSTIFY, - padding: AnyDimension = 0, - padding_char: Optional[str] = None, - padding_style: str = "", - width: AnyDimension = None, - height: AnyDimension = None, - z_index: Optional[int] = None, - modal: bool = False, - key_bindings: Optional[KeyBindingsBase] = None, - style: Union[str, Callable[[], str]] = "", - ) -> None: - - super().__init__( - children=children, - window_too_small=window_too_small, - padding=padding, - padding_char=padding_char, - padding_style=padding_style, - width=width, - height=height, - z_index=z_index, - modal=modal, - key_bindings=key_bindings, - style=style, - ) - - self.align = align - - self._children_cache: SimpleCache[ - Tuple[Container, ...], List[Container] - ] = SimpleCache(maxsize=1) - self._remaining_space_window = Window() # Dummy window. - - def preferred_width(self, max_available_width: int) -> Dimension: - if self.width is not None: - return to_dimension(self.width) - - if self.children: - dimensions = [c.preferred_width(max_available_width) for c in self.children] - return max_layout_dimensions(dimensions) - else: - return Dimension() - - def preferred_height(self, width: int, max_available_height: int) -> Dimension: - if self.height is not None: - return to_dimension(self.height) - - dimensions = [ - c.preferred_height(width, max_available_height) for c in self._all_children - ] - return sum_layout_dimensions(dimensions) - - def reset(self) -> None: - for c in self.children: - c.reset() - - @property - def _all_children(self) -> List[Container]: - """ - List of child objects, including padding. - """ - - def get() -> List[Container]: - result: List[Container] = [] - - # Padding Top. - if self.align in (VerticalAlign.CENTER, VerticalAlign.BOTTOM): - result.append(Window(width=Dimension(preferred=0))) - - # The children with padding. - for child in self.children: - result.append(child) - result.append( - Window( - height=self.padding, - char=self.padding_char, - style=self.padding_style, - ) - ) - if result: - result.pop() - - # Padding right. - if self.align in (VerticalAlign.CENTER, VerticalAlign.TOP): - result.append(Window(width=Dimension(preferred=0))) - - return result - - return self._children_cache.get(tuple(self.children), get) - - def write_to_screen( - self, - screen: Screen, - mouse_handlers: MouseHandlers, - write_position: WritePosition, - parent_style: str, - erase_bg: bool, - z_index: Optional[int], - ) -> None: - """ - Render the prompt to a `Screen` instance. - - :param screen: The :class:`~prompt_toolkit.layout.screen.Screen` class - to which the output has to be written. - """ - sizes = self._divide_heights(write_position) - style = parent_style + " " + to_str(self.style) - z_index = z_index if self.z_index is None else self.z_index - - if sizes is None: - self.window_too_small.write_to_screen( - screen, mouse_handlers, write_position, style, erase_bg, z_index - ) - else: - # - ypos = write_position.ypos - xpos = write_position.xpos - width = write_position.width - - # Draw child panes. - for s, c in zip(sizes, self._all_children): - c.write_to_screen( - screen, - mouse_handlers, - WritePosition(xpos, ypos, width, s), - style, - erase_bg, - z_index, - ) - ypos += s - - # Fill in the remaining space. This happens when a child control - # refuses to take more space and we don't have any padding. Adding a - # dummy child control for this (in `self._all_children`) is not - # desired, because in some situations, it would take more space, even - # when it's not required. This is required to apply the styling. - remaining_height = write_position.ypos + write_position.height - ypos - if remaining_height > 0: - self._remaining_space_window.write_to_screen( - screen, - mouse_handlers, - WritePosition(xpos, ypos, width, remaining_height), - style, - erase_bg, - z_index, - ) - - def _divide_heights(self, write_position: WritePosition) -> Optional[List[int]]: - """ - Return the heights for all rows. - Or None when there is not enough space. - """ - if not self.children: - return [] - - width = write_position.width - height = write_position.height - - # Calculate heights. - dimensions = [c.preferred_height(width, height) for c in self._all_children] - - # Sum dimensions - sum_dimensions = sum_layout_dimensions(dimensions) - - # If there is not enough space for both. - # Don't do anything. - if sum_dimensions.min > height: - return None - - # Find optimal sizes. (Start with minimal size, increase until we cover - # the whole height.) - sizes = [d.min for d in dimensions] - - child_generator = take_using_weights( - items=list(range(len(dimensions))), weights=[d.weight for d in dimensions] - ) - - i = next(child_generator) - - # Increase until we meet at least the 'preferred' size. - preferred_stop = min(height, sum_dimensions.preferred) - preferred_dimensions = [d.preferred for d in dimensions] - - while sum(sizes) < preferred_stop: - if sizes[i] < preferred_dimensions[i]: - sizes[i] += 1 - i = next(child_generator) - - # Increase until we use all the available space. (or until "max") - if not get_app().is_done: - max_stop = min(height, sum_dimensions.max) - max_dimensions = [d.max for d in dimensions] - - while sum(sizes) < max_stop: - if sizes[i] < max_dimensions[i]: - sizes[i] += 1 - i = next(child_generator) - - return sizes - - -class VSplit(_Split): - """ - Several layouts, one stacked left/right of the other. :: - - +---------+----------+ - | | | - | | | - +---------+----------+ - - By default, this doesn't display a vertical line between the children, but - if this is something you need, then create a HSplit as follows:: - - VSplit(children=[ ... ], padding_char='|', - padding=1, padding_style='#ffff00') - - :param children: List of child :class:`.Container` objects. - :param window_too_small: A :class:`.Container` object that is displayed if - there is not enough space for all the children. By default, this is a - "Window too small" message. - :param align: `HorizontalAlign` value. - :param width: When given, use this width instead of looking at the children. - :param height: When given, use this height instead of looking at the children. - :param z_index: (int or None) When specified, this can be used to bring - element in front of floating elements. `None` means: inherit from parent. - :param style: A style string. - :param modal: ``True`` or ``False``. - :param key_bindings: ``None`` or a :class:`.KeyBindings` object. - - :param padding: (`Dimension` or int), size to be used for the padding. - :param padding_char: Character to be used for filling in the padding. - :param padding_style: Style to applied to the padding. - """ - - def __init__( - self, - children: Sequence[AnyContainer], - window_too_small: Optional[Container] = None, - align: HorizontalAlign = HorizontalAlign.JUSTIFY, - padding: AnyDimension = 0, - padding_char: Optional[str] = None, - padding_style: str = "", - width: AnyDimension = None, - height: AnyDimension = None, - z_index: Optional[int] = None, - modal: bool = False, - key_bindings: Optional[KeyBindingsBase] = None, - style: Union[str, Callable[[], str]] = "", - ) -> None: - - super().__init__( - children=children, - window_too_small=window_too_small, - padding=padding, - padding_char=padding_char, - padding_style=padding_style, - width=width, - height=height, - z_index=z_index, - modal=modal, - key_bindings=key_bindings, - style=style, - ) - - self.align = align - - self._children_cache: SimpleCache[ - Tuple[Container, ...], List[Container] - ] = SimpleCache(maxsize=1) - self._remaining_space_window = Window() # Dummy window. - - def preferred_width(self, max_available_width: int) -> Dimension: - if self.width is not None: - return to_dimension(self.width) - - dimensions = [ - c.preferred_width(max_available_width) for c in self._all_children - ] - - return sum_layout_dimensions(dimensions) - - def preferred_height(self, width: int, max_available_height: int) -> Dimension: - if self.height is not None: - return to_dimension(self.height) - - # At the point where we want to calculate the heights, the widths have - # already been decided. So we can trust `width` to be the actual - # `width` that's going to be used for the rendering. So, - # `divide_widths` is supposed to use all of the available width. - # Using only the `preferred` width caused a bug where the reported - # height was more than required. (we had a `BufferControl` which did - # wrap lines because of the smaller width returned by `_divide_widths`. - - sizes = self._divide_widths(width) - children = self._all_children - - if sizes is None: - return Dimension() - else: - dimensions = [ - c.preferred_height(s, max_available_height) - for s, c in zip(sizes, children) - ] - return max_layout_dimensions(dimensions) - - def reset(self) -> None: - for c in self.children: - c.reset() - - @property - def _all_children(self) -> List[Container]: - """ - List of child objects, including padding. - """ - - def get() -> List[Container]: - result: List[Container] = [] - - # Padding left. - if self.align in (HorizontalAlign.CENTER, HorizontalAlign.RIGHT): - result.append(Window(width=Dimension(preferred=0))) - - # The children with padding. - for child in self.children: - result.append(child) - result.append( - Window( - width=self.padding, - char=self.padding_char, - style=self.padding_style, - ) - ) - if result: - result.pop() - - # Padding right. - if self.align in (HorizontalAlign.CENTER, HorizontalAlign.LEFT): - result.append(Window(width=Dimension(preferred=0))) - - return result - - return self._children_cache.get(tuple(self.children), get) - - def _divide_widths(self, width: int) -> Optional[List[int]]: - """ - Return the widths for all columns. - Or None when there is not enough space. - """ - children = self._all_children - - if not children: - return [] - - # Calculate widths. - dimensions = [c.preferred_width(width) for c in children] - preferred_dimensions = [d.preferred for d in dimensions] - - # Sum dimensions - sum_dimensions = sum_layout_dimensions(dimensions) - - # If there is not enough space for both. - # Don't do anything. - if sum_dimensions.min > width: - return None - - # Find optimal sizes. (Start with minimal size, increase until we cover - # the whole width.) - sizes = [d.min for d in dimensions] - - child_generator = take_using_weights( - items=list(range(len(dimensions))), weights=[d.weight for d in dimensions] - ) - - i = next(child_generator) - - # Increase until we meet at least the 'preferred' size. - preferred_stop = min(width, sum_dimensions.preferred) - - while sum(sizes) < preferred_stop: - if sizes[i] < preferred_dimensions[i]: - sizes[i] += 1 - i = next(child_generator) - - # Increase until we use all the available space. - max_dimensions = [d.max for d in dimensions] - max_stop = min(width, sum_dimensions.max) - - while sum(sizes) < max_stop: - if sizes[i] < max_dimensions[i]: - sizes[i] += 1 - i = next(child_generator) - - return sizes - - def write_to_screen( - self, - screen: Screen, - mouse_handlers: MouseHandlers, - write_position: WritePosition, - parent_style: str, - erase_bg: bool, - z_index: Optional[int], - ) -> None: - """ - Render the prompt to a `Screen` instance. - - :param screen: The :class:`~prompt_toolkit.layout.screen.Screen` class - to which the output has to be written. - """ - if not self.children: - return - - children = self._all_children - sizes = self._divide_widths(write_position.width) - style = parent_style + " " + to_str(self.style) - z_index = z_index if self.z_index is None else self.z_index - - # If there is not enough space. - if sizes is None: - self.window_too_small.write_to_screen( - screen, mouse_handlers, write_position, style, erase_bg, z_index - ) - return - - # Calculate heights, take the largest possible, but not larger than - # write_position.height. - heights = [ - child.preferred_height(width, write_position.height).preferred - for width, child in zip(sizes, children) - ] - height = max(write_position.height, min(write_position.height, max(heights))) - - # - ypos = write_position.ypos - xpos = write_position.xpos - - # Draw all child panes. - for s, c in zip(sizes, children): - c.write_to_screen( - screen, - mouse_handlers, - WritePosition(xpos, ypos, s, height), - style, - erase_bg, - z_index, - ) - xpos += s - - # Fill in the remaining space. This happens when a child control - # refuses to take more space and we don't have any padding. Adding a - # dummy child control for this (in `self._all_children`) is not - # desired, because in some situations, it would take more space, even - # when it's not required. This is required to apply the styling. - remaining_width = write_position.xpos + write_position.width - xpos - if remaining_width > 0: - self._remaining_space_window.write_to_screen( - screen, - mouse_handlers, - WritePosition(xpos, ypos, remaining_width, height), - style, - erase_bg, - z_index, - ) - - -class FloatContainer(Container): - """ - Container which can contain another container for the background, as well - as a list of floating containers on top of it. - - Example Usage:: - - FloatContainer(content=Window(...), - floats=[ - Float(xcursor=True, - ycursor=True, - content=CompletionsMenu(...)) - ]) - - :param z_index: (int or None) When specified, this can be used to bring - element in front of floating elements. `None` means: inherit from parent. - This is the z_index for the whole `Float` container as a whole. - """ - - def __init__( - self, - content: AnyContainer, - floats: List["Float"], - modal: bool = False, - key_bindings: Optional[KeyBindingsBase] = None, - style: Union[str, Callable[[], str]] = "", - z_index: Optional[int] = None, - ) -> None: - - self.content = to_container(content) - self.floats = floats - - self.modal = modal - self.key_bindings = key_bindings - self.style = style - self.z_index = z_index - - def reset(self) -> None: - self.content.reset() - - for f in self.floats: - f.content.reset() - - def preferred_width(self, max_available_width: int) -> Dimension: - return self.content.preferred_width(max_available_width) - - def preferred_height(self, width: int, max_available_height: int) -> Dimension: - """ - Return the preferred height of the float container. - (We don't care about the height of the floats, they should always fit - into the dimensions provided by the container.) - """ - return self.content.preferred_height(width, max_available_height) - - def write_to_screen( - self, - screen: Screen, - mouse_handlers: MouseHandlers, - write_position: WritePosition, - parent_style: str, - erase_bg: bool, - z_index: Optional[int], - ) -> None: - style = parent_style + " " + to_str(self.style) - z_index = z_index if self.z_index is None else self.z_index - - self.content.write_to_screen( - screen, mouse_handlers, write_position, style, erase_bg, z_index - ) - - for number, fl in enumerate(self.floats): - # z_index of a Float is computed by summing the z_index of the - # container and the `Float`. - new_z_index = (z_index or 0) + fl.z_index - style = parent_style + " " + to_str(self.style) - - # If the float that we have here, is positioned relative to the - # cursor position, but the Window that specifies the cursor - # position is not drawn yet, because it's a Float itself, we have - # to postpone this calculation. (This is a work-around, but good - # enough for now.) - postpone = fl.xcursor is not None or fl.ycursor is not None - - if postpone: - new_z_index = ( +""" +Container for the layout. +(Containers can contain other containers or user interface controls.) +""" +from abc import ABCMeta, abstractmethod +from enum import Enum +from functools import partial +from typing import ( + TYPE_CHECKING, + Callable, + Dict, + List, + Optional, + Sequence, + Tuple, + Union, + cast, +) + +from prompt_toolkit.application.current import get_app +from prompt_toolkit.cache import SimpleCache +from prompt_toolkit.data_structures import Point +from prompt_toolkit.filters import ( + FilterOrBool, + emacs_insert_mode, + to_filter, + vi_insert_mode, +) +from prompt_toolkit.formatted_text import ( + AnyFormattedText, + StyleAndTextTuples, + to_formatted_text, +) +from prompt_toolkit.formatted_text.utils import ( + fragment_list_to_text, + fragment_list_width, +) +from prompt_toolkit.key_binding import KeyBindingsBase +from prompt_toolkit.mouse_events import MouseEvent, MouseEventType +from prompt_toolkit.utils import get_cwidth, take_using_weights, to_int, to_str + +from .controls import ( + DummyControl, + FormattedTextControl, + GetLinePrefixCallable, + UIContent, + UIControl, +) +from .dimension import ( + AnyDimension, + Dimension, + max_layout_dimensions, + sum_layout_dimensions, + to_dimension, +) +from .margins import Margin +from .mouse_handlers import MouseHandlers +from .screen import _CHAR_CACHE, Screen, WritePosition +from .utils import explode_text_fragments + +if TYPE_CHECKING: + from typing_extensions import Protocol, TypeGuard + + from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone + + +__all__ = [ + "AnyContainer", + "Container", + "HorizontalAlign", + "VerticalAlign", + "HSplit", + "VSplit", + "FloatContainer", + "Float", + "WindowAlign", + "Window", + "WindowRenderInfo", + "ConditionalContainer", + "ScrollOffsets", + "ColorColumn", + "to_container", + "to_window", + "is_container", + "DynamicContainer", +] + + +class Container(metaclass=ABCMeta): + """ + Base class for user interface layout. + """ + + @abstractmethod + def reset(self) -> None: + """ + Reset the state of this container and all the children. + (E.g. reset scroll offsets, etc...) + """ + + @abstractmethod + def preferred_width(self, max_available_width: int) -> Dimension: + """ + Return a :class:`~prompt_toolkit.layout.Dimension` that represents the + desired width for this container. + """ + + @abstractmethod + def preferred_height(self, width: int, max_available_height: int) -> Dimension: + """ + Return a :class:`~prompt_toolkit.layout.Dimension` that represents the + desired height for this container. + """ + + @abstractmethod + def write_to_screen( + self, + screen: Screen, + mouse_handlers: MouseHandlers, + write_position: WritePosition, + parent_style: str, + erase_bg: bool, + z_index: Optional[int], + ) -> None: + """ + Write the actual content to the screen. + + :param screen: :class:`~prompt_toolkit.layout.screen.Screen` + :param mouse_handlers: :class:`~prompt_toolkit.layout.mouse_handlers.MouseHandlers`. + :param parent_style: Style string to pass to the :class:`.Window` + object. This will be applied to all content of the windows. + :class:`.VSplit` and :class:`.HSplit` can use it to pass their + style down to the windows that they contain. + :param z_index: Used for propagating z_index from parent to child. + """ + + def is_modal(self) -> bool: + """ + When this container is modal, key bindings from parent containers are + not taken into account if a user control in this container is focused. + """ + return False + + def get_key_bindings(self) -> Optional[KeyBindingsBase]: + """ + Returns a :class:`.KeyBindings` object. These bindings become active when any + user control in this container has the focus, except if any containers + between this container and the focused user control is modal. + """ + return None + + @abstractmethod + def get_children(self) -> List["Container"]: + """ + Return the list of child :class:`.Container` objects. + """ + return [] + + +if TYPE_CHECKING: + + class MagicContainer(Protocol): + """ + Any object that implements ``__pt_container__`` represents a container. + """ + + def __pt_container__(self) -> "AnyContainer": + ... + + +AnyContainer = Union[Container, "MagicContainer"] + + +def _window_too_small() -> "Window": + "Create a `Window` that displays the 'Window too small' text." + return Window( + FormattedTextControl(text=[("class:window-too-small", " Window too small... ")]) + ) + + +class VerticalAlign(Enum): + "Alignment for `HSplit`." + TOP = "TOP" + CENTER = "CENTER" + BOTTOM = "BOTTOM" + JUSTIFY = "JUSTIFY" + + +class HorizontalAlign(Enum): + "Alignment for `VSplit`." + LEFT = "LEFT" + CENTER = "CENTER" + RIGHT = "RIGHT" + JUSTIFY = "JUSTIFY" + + +class _Split(Container): + """ + The common parts of `VSplit` and `HSplit`. + """ + + def __init__( + self, + children: Sequence[AnyContainer], + window_too_small: Optional[Container] = None, + padding: AnyDimension = Dimension.exact(0), + padding_char: Optional[str] = None, + padding_style: str = "", + width: AnyDimension = None, + height: AnyDimension = None, + z_index: Optional[int] = None, + modal: bool = False, + key_bindings: Optional[KeyBindingsBase] = None, + style: Union[str, Callable[[], str]] = "", + ) -> None: + + self.children = [to_container(c) for c in children] + self.window_too_small = window_too_small or _window_too_small() + self.padding = padding + self.padding_char = padding_char + self.padding_style = padding_style + + self.width = width + self.height = height + self.z_index = z_index + + self.modal = modal + self.key_bindings = key_bindings + self.style = style + + def is_modal(self) -> bool: + return self.modal + + def get_key_bindings(self) -> Optional[KeyBindingsBase]: + return self.key_bindings + + def get_children(self) -> List[Container]: + return self.children + + +class HSplit(_Split): + """ + Several layouts, one stacked above/under the other. :: + + +--------------------+ + | | + +--------------------+ + | | + +--------------------+ + + By default, this doesn't display a horizontal line between the children, + but if this is something you need, then create a HSplit as follows:: + + HSplit(children=[ ... ], padding_char='-', + padding=1, padding_style='#ffff00') + + :param children: List of child :class:`.Container` objects. + :param window_too_small: A :class:`.Container` object that is displayed if + there is not enough space for all the children. By default, this is a + "Window too small" message. + :param align: `VerticalAlign` value. + :param width: When given, use this width instead of looking at the children. + :param height: When given, use this height instead of looking at the children. + :param z_index: (int or None) When specified, this can be used to bring + element in front of floating elements. `None` means: inherit from parent. + :param style: A style string. + :param modal: ``True`` or ``False``. + :param key_bindings: ``None`` or a :class:`.KeyBindings` object. + + :param padding: (`Dimension` or int), size to be used for the padding. + :param padding_char: Character to be used for filling in the padding. + :param padding_style: Style to applied to the padding. + """ + + def __init__( + self, + children: Sequence[AnyContainer], + window_too_small: Optional[Container] = None, + align: VerticalAlign = VerticalAlign.JUSTIFY, + padding: AnyDimension = 0, + padding_char: Optional[str] = None, + padding_style: str = "", + width: AnyDimension = None, + height: AnyDimension = None, + z_index: Optional[int] = None, + modal: bool = False, + key_bindings: Optional[KeyBindingsBase] = None, + style: Union[str, Callable[[], str]] = "", + ) -> None: + + super().__init__( + children=children, + window_too_small=window_too_small, + padding=padding, + padding_char=padding_char, + padding_style=padding_style, + width=width, + height=height, + z_index=z_index, + modal=modal, + key_bindings=key_bindings, + style=style, + ) + + self.align = align + + self._children_cache: SimpleCache[ + Tuple[Container, ...], List[Container] + ] = SimpleCache(maxsize=1) + self._remaining_space_window = Window() # Dummy window. + + def preferred_width(self, max_available_width: int) -> Dimension: + if self.width is not None: + return to_dimension(self.width) + + if self.children: + dimensions = [c.preferred_width(max_available_width) for c in self.children] + return max_layout_dimensions(dimensions) + else: + return Dimension() + + def preferred_height(self, width: int, max_available_height: int) -> Dimension: + if self.height is not None: + return to_dimension(self.height) + + dimensions = [ + c.preferred_height(width, max_available_height) for c in self._all_children + ] + return sum_layout_dimensions(dimensions) + + def reset(self) -> None: + for c in self.children: + c.reset() + + @property + def _all_children(self) -> List[Container]: + """ + List of child objects, including padding. + """ + + def get() -> List[Container]: + result: List[Container] = [] + + # Padding Top. + if self.align in (VerticalAlign.CENTER, VerticalAlign.BOTTOM): + result.append(Window(width=Dimension(preferred=0))) + + # The children with padding. + for child in self.children: + result.append(child) + result.append( + Window( + height=self.padding, + char=self.padding_char, + style=self.padding_style, + ) + ) + if result: + result.pop() + + # Padding right. + if self.align in (VerticalAlign.CENTER, VerticalAlign.TOP): + result.append(Window(width=Dimension(preferred=0))) + + return result + + return self._children_cache.get(tuple(self.children), get) + + def write_to_screen( + self, + screen: Screen, + mouse_handlers: MouseHandlers, + write_position: WritePosition, + parent_style: str, + erase_bg: bool, + z_index: Optional[int], + ) -> None: + """ + Render the prompt to a `Screen` instance. + + :param screen: The :class:`~prompt_toolkit.layout.screen.Screen` class + to which the output has to be written. + """ + sizes = self._divide_heights(write_position) + style = parent_style + " " + to_str(self.style) + z_index = z_index if self.z_index is None else self.z_index + + if sizes is None: + self.window_too_small.write_to_screen( + screen, mouse_handlers, write_position, style, erase_bg, z_index + ) + else: + # + ypos = write_position.ypos + xpos = write_position.xpos + width = write_position.width + + # Draw child panes. + for s, c in zip(sizes, self._all_children): + c.write_to_screen( + screen, + mouse_handlers, + WritePosition(xpos, ypos, width, s), + style, + erase_bg, + z_index, + ) + ypos += s + + # Fill in the remaining space. This happens when a child control + # refuses to take more space and we don't have any padding. Adding a + # dummy child control for this (in `self._all_children`) is not + # desired, because in some situations, it would take more space, even + # when it's not required. This is required to apply the styling. + remaining_height = write_position.ypos + write_position.height - ypos + if remaining_height > 0: + self._remaining_space_window.write_to_screen( + screen, + mouse_handlers, + WritePosition(xpos, ypos, width, remaining_height), + style, + erase_bg, + z_index, + ) + + def _divide_heights(self, write_position: WritePosition) -> Optional[List[int]]: + """ + Return the heights for all rows. + Or None when there is not enough space. + """ + if not self.children: + return [] + + width = write_position.width + height = write_position.height + + # Calculate heights. + dimensions = [c.preferred_height(width, height) for c in self._all_children] + + # Sum dimensions + sum_dimensions = sum_layout_dimensions(dimensions) + + # If there is not enough space for both. + # Don't do anything. + if sum_dimensions.min > height: + return None + + # Find optimal sizes. (Start with minimal size, increase until we cover + # the whole height.) + sizes = [d.min for d in dimensions] + + child_generator = take_using_weights( + items=list(range(len(dimensions))), weights=[d.weight for d in dimensions] + ) + + i = next(child_generator) + + # Increase until we meet at least the 'preferred' size. + preferred_stop = min(height, sum_dimensions.preferred) + preferred_dimensions = [d.preferred for d in dimensions] + + while sum(sizes) < preferred_stop: + if sizes[i] < preferred_dimensions[i]: + sizes[i] += 1 + i = next(child_generator) + + # Increase until we use all the available space. (or until "max") + if not get_app().is_done: + max_stop = min(height, sum_dimensions.max) + max_dimensions = [d.max for d in dimensions] + + while sum(sizes) < max_stop: + if sizes[i] < max_dimensions[i]: + sizes[i] += 1 + i = next(child_generator) + + return sizes + + +class VSplit(_Split): + """ + Several layouts, one stacked left/right of the other. :: + + +---------+----------+ + | | | + | | | + +---------+----------+ + + By default, this doesn't display a vertical line between the children, but + if this is something you need, then create a HSplit as follows:: + + VSplit(children=[ ... ], padding_char='|', + padding=1, padding_style='#ffff00') + + :param children: List of child :class:`.Container` objects. + :param window_too_small: A :class:`.Container` object that is displayed if + there is not enough space for all the children. By default, this is a + "Window too small" message. + :param align: `HorizontalAlign` value. + :param width: When given, use this width instead of looking at the children. + :param height: When given, use this height instead of looking at the children. + :param z_index: (int or None) When specified, this can be used to bring + element in front of floating elements. `None` means: inherit from parent. + :param style: A style string. + :param modal: ``True`` or ``False``. + :param key_bindings: ``None`` or a :class:`.KeyBindings` object. + + :param padding: (`Dimension` or int), size to be used for the padding. + :param padding_char: Character to be used for filling in the padding. + :param padding_style: Style to applied to the padding. + """ + + def __init__( + self, + children: Sequence[AnyContainer], + window_too_small: Optional[Container] = None, + align: HorizontalAlign = HorizontalAlign.JUSTIFY, + padding: AnyDimension = 0, + padding_char: Optional[str] = None, + padding_style: str = "", + width: AnyDimension = None, + height: AnyDimension = None, + z_index: Optional[int] = None, + modal: bool = False, + key_bindings: Optional[KeyBindingsBase] = None, + style: Union[str, Callable[[], str]] = "", + ) -> None: + + super().__init__( + children=children, + window_too_small=window_too_small, + padding=padding, + padding_char=padding_char, + padding_style=padding_style, + width=width, + height=height, + z_index=z_index, + modal=modal, + key_bindings=key_bindings, + style=style, + ) + + self.align = align + + self._children_cache: SimpleCache[ + Tuple[Container, ...], List[Container] + ] = SimpleCache(maxsize=1) + self._remaining_space_window = Window() # Dummy window. + + def preferred_width(self, max_available_width: int) -> Dimension: + if self.width is not None: + return to_dimension(self.width) + + dimensions = [ + c.preferred_width(max_available_width) for c in self._all_children + ] + + return sum_layout_dimensions(dimensions) + + def preferred_height(self, width: int, max_available_height: int) -> Dimension: + if self.height is not None: + return to_dimension(self.height) + + # At the point where we want to calculate the heights, the widths have + # already been decided. So we can trust `width` to be the actual + # `width` that's going to be used for the rendering. So, + # `divide_widths` is supposed to use all of the available width. + # Using only the `preferred` width caused a bug where the reported + # height was more than required. (we had a `BufferControl` which did + # wrap lines because of the smaller width returned by `_divide_widths`. + + sizes = self._divide_widths(width) + children = self._all_children + + if sizes is None: + return Dimension() + else: + dimensions = [ + c.preferred_height(s, max_available_height) + for s, c in zip(sizes, children) + ] + return max_layout_dimensions(dimensions) + + def reset(self) -> None: + for c in self.children: + c.reset() + + @property + def _all_children(self) -> List[Container]: + """ + List of child objects, including padding. + """ + + def get() -> List[Container]: + result: List[Container] = [] + + # Padding left. + if self.align in (HorizontalAlign.CENTER, HorizontalAlign.RIGHT): + result.append(Window(width=Dimension(preferred=0))) + + # The children with padding. + for child in self.children: + result.append(child) + result.append( + Window( + width=self.padding, + char=self.padding_char, + style=self.padding_style, + ) + ) + if result: + result.pop() + + # Padding right. + if self.align in (HorizontalAlign.CENTER, HorizontalAlign.LEFT): + result.append(Window(width=Dimension(preferred=0))) + + return result + + return self._children_cache.get(tuple(self.children), get) + + def _divide_widths(self, width: int) -> Optional[List[int]]: + """ + Return the widths for all columns. + Or None when there is not enough space. + """ + children = self._all_children + + if not children: + return [] + + # Calculate widths. + dimensions = [c.preferred_width(width) for c in children] + preferred_dimensions = [d.preferred for d in dimensions] + + # Sum dimensions + sum_dimensions = sum_layout_dimensions(dimensions) + + # If there is not enough space for both. + # Don't do anything. + if sum_dimensions.min > width: + return None + + # Find optimal sizes. (Start with minimal size, increase until we cover + # the whole width.) + sizes = [d.min for d in dimensions] + + child_generator = take_using_weights( + items=list(range(len(dimensions))), weights=[d.weight for d in dimensions] + ) + + i = next(child_generator) + + # Increase until we meet at least the 'preferred' size. + preferred_stop = min(width, sum_dimensions.preferred) + + while sum(sizes) < preferred_stop: + if sizes[i] < preferred_dimensions[i]: + sizes[i] += 1 + i = next(child_generator) + + # Increase until we use all the available space. + max_dimensions = [d.max for d in dimensions] + max_stop = min(width, sum_dimensions.max) + + while sum(sizes) < max_stop: + if sizes[i] < max_dimensions[i]: + sizes[i] += 1 + i = next(child_generator) + + return sizes + + def write_to_screen( + self, + screen: Screen, + mouse_handlers: MouseHandlers, + write_position: WritePosition, + parent_style: str, + erase_bg: bool, + z_index: Optional[int], + ) -> None: + """ + Render the prompt to a `Screen` instance. + + :param screen: The :class:`~prompt_toolkit.layout.screen.Screen` class + to which the output has to be written. + """ + if not self.children: + return + + children = self._all_children + sizes = self._divide_widths(write_position.width) + style = parent_style + " " + to_str(self.style) + z_index = z_index if self.z_index is None else self.z_index + + # If there is not enough space. + if sizes is None: + self.window_too_small.write_to_screen( + screen, mouse_handlers, write_position, style, erase_bg, z_index + ) + return + + # Calculate heights, take the largest possible, but not larger than + # write_position.height. + heights = [ + child.preferred_height(width, write_position.height).preferred + for width, child in zip(sizes, children) + ] + height = max(write_position.height, min(write_position.height, max(heights))) + + # + ypos = write_position.ypos + xpos = write_position.xpos + + # Draw all child panes. + for s, c in zip(sizes, children): + c.write_to_screen( + screen, + mouse_handlers, + WritePosition(xpos, ypos, s, height), + style, + erase_bg, + z_index, + ) + xpos += s + + # Fill in the remaining space. This happens when a child control + # refuses to take more space and we don't have any padding. Adding a + # dummy child control for this (in `self._all_children`) is not + # desired, because in some situations, it would take more space, even + # when it's not required. This is required to apply the styling. + remaining_width = write_position.xpos + write_position.width - xpos + if remaining_width > 0: + self._remaining_space_window.write_to_screen( + screen, + mouse_handlers, + WritePosition(xpos, ypos, remaining_width, height), + style, + erase_bg, + z_index, + ) + + +class FloatContainer(Container): + """ + Container which can contain another container for the background, as well + as a list of floating containers on top of it. + + Example Usage:: + + FloatContainer(content=Window(...), + floats=[ + Float(xcursor=True, + ycursor=True, + content=CompletionsMenu(...)) + ]) + + :param z_index: (int or None) When specified, this can be used to bring + element in front of floating elements. `None` means: inherit from parent. + This is the z_index for the whole `Float` container as a whole. + """ + + def __init__( + self, + content: AnyContainer, + floats: List["Float"], + modal: bool = False, + key_bindings: Optional[KeyBindingsBase] = None, + style: Union[str, Callable[[], str]] = "", + z_index: Optional[int] = None, + ) -> None: + + self.content = to_container(content) + self.floats = floats + + self.modal = modal + self.key_bindings = key_bindings + self.style = style + self.z_index = z_index + + def reset(self) -> None: + self.content.reset() + + for f in self.floats: + f.content.reset() + + def preferred_width(self, max_available_width: int) -> Dimension: + return self.content.preferred_width(max_available_width) + + def preferred_height(self, width: int, max_available_height: int) -> Dimension: + """ + Return the preferred height of the float container. + (We don't care about the height of the floats, they should always fit + into the dimensions provided by the container.) + """ + return self.content.preferred_height(width, max_available_height) + + def write_to_screen( + self, + screen: Screen, + mouse_handlers: MouseHandlers, + write_position: WritePosition, + parent_style: str, + erase_bg: bool, + z_index: Optional[int], + ) -> None: + style = parent_style + " " + to_str(self.style) + z_index = z_index if self.z_index is None else self.z_index + + self.content.write_to_screen( + screen, mouse_handlers, write_position, style, erase_bg, z_index + ) + + for number, fl in enumerate(self.floats): + # z_index of a Float is computed by summing the z_index of the + # container and the `Float`. + new_z_index = (z_index or 0) + fl.z_index + style = parent_style + " " + to_str(self.style) + + # If the float that we have here, is positioned relative to the + # cursor position, but the Window that specifies the cursor + # position is not drawn yet, because it's a Float itself, we have + # to postpone this calculation. (This is a work-around, but good + # enough for now.) + postpone = fl.xcursor is not None or fl.ycursor is not None + + if postpone: + new_z_index = ( number + 10**8 - ) # Draw as late as possible, but keep the order. - screen.draw_with_z_index( - z_index=new_z_index, - draw_func=partial( - self._draw_float, - fl, - screen, - mouse_handlers, - write_position, - style, - erase_bg, - new_z_index, - ), - ) - else: - self._draw_float( - fl, - screen, - mouse_handlers, - write_position, - style, - erase_bg, - new_z_index, - ) - - def _draw_float( - self, - fl: "Float", - screen: Screen, - mouse_handlers: MouseHandlers, - write_position: WritePosition, - style: str, - erase_bg: bool, - z_index: Optional[int], - ) -> None: - "Draw a single Float." - # When a menu_position was given, use this instead of the cursor - # position. (These cursor positions are absolute, translate again - # relative to the write_position.) - # Note: This should be inside the for-loop, because one float could - # set the cursor position to be used for the next one. - cpos = screen.get_menu_position( - fl.attach_to_window or get_app().layout.current_window - ) - cursor_position = Point( - x=cpos.x - write_position.xpos, y=cpos.y - write_position.ypos - ) - - fl_width = fl.get_width() - fl_height = fl.get_height() - width: int - height: int - xpos: int - ypos: int - - # Left & width given. - if fl.left is not None and fl_width is not None: - xpos = fl.left - width = fl_width - # Left & right given -> calculate width. - elif fl.left is not None and fl.right is not None: - xpos = fl.left - width = write_position.width - fl.left - fl.right - # Width & right given -> calculate left. - elif fl_width is not None and fl.right is not None: - xpos = write_position.width - fl.right - fl_width - width = fl_width - # Near x position of cursor. - elif fl.xcursor: - if fl_width is None: - width = fl.content.preferred_width(write_position.width).preferred - width = min(write_position.width, width) - else: - width = fl_width - - xpos = cursor_position.x - if xpos + width > write_position.width: - xpos = max(0, write_position.width - width) - # Only width given -> center horizontally. - elif fl_width: - xpos = int((write_position.width - fl_width) / 2) - width = fl_width - # Otherwise, take preferred width from float content. - else: - width = fl.content.preferred_width(write_position.width).preferred - - if fl.left is not None: - xpos = fl.left - elif fl.right is not None: - xpos = max(0, write_position.width - width - fl.right) - else: # Center horizontally. - xpos = max(0, int((write_position.width - width) / 2)) - - # Trim. - width = min(width, write_position.width - xpos) - - # Top & height given. - if fl.top is not None and fl_height is not None: - ypos = fl.top - height = fl_height - # Top & bottom given -> calculate height. - elif fl.top is not None and fl.bottom is not None: - ypos = fl.top - height = write_position.height - fl.top - fl.bottom - # Height & bottom given -> calculate top. - elif fl_height is not None and fl.bottom is not None: - ypos = write_position.height - fl_height - fl.bottom - height = fl_height - # Near cursor. - elif fl.ycursor: - ypos = cursor_position.y + (0 if fl.allow_cover_cursor else 1) - - if fl_height is None: - height = fl.content.preferred_height( - width, write_position.height - ).preferred - else: - height = fl_height - - # Reduce height if not enough space. (We can use the height - # when the content requires it.) - if height > write_position.height - ypos: - if write_position.height - ypos + 1 >= ypos: - # When the space below the cursor is more than - # the space above, just reduce the height. - height = write_position.height - ypos - else: - # Otherwise, fit the float above the cursor. - height = min(height, cursor_position.y) - ypos = cursor_position.y - height - - # Only height given -> center vertically. - elif fl_height: - ypos = int((write_position.height - fl_height) / 2) - height = fl_height - # Otherwise, take preferred height from content. - else: - height = fl.content.preferred_height(width, write_position.height).preferred - - if fl.top is not None: - ypos = fl.top - elif fl.bottom is not None: - ypos = max(0, write_position.height - height - fl.bottom) - else: # Center vertically. - ypos = max(0, int((write_position.height - height) / 2)) - - # Trim. - height = min(height, write_position.height - ypos) - - # Write float. - # (xpos and ypos can be negative: a float can be partially visible.) - if height > 0 and width > 0: - wp = WritePosition( - xpos=xpos + write_position.xpos, - ypos=ypos + write_position.ypos, - width=width, - height=height, - ) - - if not fl.hide_when_covering_content or self._area_is_empty(screen, wp): - fl.content.write_to_screen( - screen, - mouse_handlers, - wp, - style, - erase_bg=not fl.transparent(), - z_index=z_index, - ) - - def _area_is_empty(self, screen: Screen, write_position: WritePosition) -> bool: - """ - Return True when the area below the write position is still empty. - (For floats that should not hide content underneath.) - """ - wp = write_position - - for y in range(wp.ypos, wp.ypos + wp.height): - if y in screen.data_buffer: - row = screen.data_buffer[y] - - for x in range(wp.xpos, wp.xpos + wp.width): - c = row[x] - if c.char != " ": - return False - - return True - - def is_modal(self) -> bool: - return self.modal - - def get_key_bindings(self) -> Optional[KeyBindingsBase]: - return self.key_bindings - - def get_children(self) -> List[Container]: - children = [self.content] - children.extend(f.content for f in self.floats) - return children - - -class Float: - """ - Float for use in a :class:`.FloatContainer`. - Except for the `content` parameter, all other options are optional. - - :param content: :class:`.Container` instance. - - :param width: :class:`.Dimension` or callable which returns a :class:`.Dimension`. - :param height: :class:`.Dimension` or callable which returns a :class:`.Dimension`. - - :param left: Distance to the left edge of the :class:`.FloatContainer`. - :param right: Distance to the right edge of the :class:`.FloatContainer`. - :param top: Distance to the top of the :class:`.FloatContainer`. - :param bottom: Distance to the bottom of the :class:`.FloatContainer`. - - :param attach_to_window: Attach to the cursor from this window, instead of - the current window. - :param hide_when_covering_content: Hide the float when it covers content underneath. - :param allow_cover_cursor: When `False`, make sure to display the float - below the cursor. Not on top of the indicated position. - :param z_index: Z-index position. For a Float, this needs to be at least - one. It is relative to the z_index of the parent container. - :param transparent: :class:`.Filter` indicating whether this float needs to be - drawn transparently. - """ - - def __init__( - self, - content: AnyContainer, - top: Optional[int] = None, - right: Optional[int] = None, - bottom: Optional[int] = None, - left: Optional[int] = None, - width: Optional[Union[int, Callable[[], int]]] = None, - height: Optional[Union[int, Callable[[], int]]] = None, - xcursor: bool = False, - ycursor: bool = False, - attach_to_window: Optional[AnyContainer] = None, - hide_when_covering_content: bool = False, - allow_cover_cursor: bool = False, - z_index: int = 1, - transparent: bool = False, - ) -> None: - - assert z_index >= 1 - - self.left = left - self.right = right - self.top = top - self.bottom = bottom - - self.width = width - self.height = height - - self.xcursor = xcursor - self.ycursor = ycursor - - self.attach_to_window = ( - to_window(attach_to_window) if attach_to_window else None - ) - - self.content = to_container(content) - self.hide_when_covering_content = hide_when_covering_content - self.allow_cover_cursor = allow_cover_cursor - self.z_index = z_index - self.transparent = to_filter(transparent) - - def get_width(self) -> Optional[int]: - if callable(self.width): - return self.width() - return self.width - - def get_height(self) -> Optional[int]: - if callable(self.height): - return self.height() - return self.height - - def __repr__(self) -> str: - return "Float(content=%r)" % self.content - - -class WindowRenderInfo: - """ - Render information for the last render time of this control. - It stores mapping information between the input buffers (in case of a - :class:`~prompt_toolkit.layout.controls.BufferControl`) and the actual - render position on the output screen. - - (Could be used for implementation of the Vi 'H' and 'L' key bindings as - well as implementing mouse support.) - - :param ui_content: The original :class:`.UIContent` instance that contains - the whole input, without clipping. (ui_content) - :param horizontal_scroll: The horizontal scroll of the :class:`.Window` instance. - :param vertical_scroll: The vertical scroll of the :class:`.Window` instance. - :param window_width: The width of the window that displays the content, - without the margins. - :param window_height: The height of the window that displays the content. - :param configured_scroll_offsets: The scroll offsets as configured for the - :class:`Window` instance. - :param visible_line_to_row_col: Mapping that maps the row numbers on the - displayed screen (starting from zero for the first visible line) to - (row, col) tuples pointing to the row and column of the :class:`.UIContent`. - :param rowcol_to_yx: Mapping that maps (row, column) tuples representing - coordinates of the :class:`UIContent` to (y, x) absolute coordinates at - the rendered screen. - """ - - def __init__( - self, - window: "Window", - ui_content: UIContent, - horizontal_scroll: int, - vertical_scroll: int, - window_width: int, - window_height: int, - configured_scroll_offsets: "ScrollOffsets", - visible_line_to_row_col: Dict[int, Tuple[int, int]], - rowcol_to_yx: Dict[Tuple[int, int], Tuple[int, int]], - x_offset: int, - y_offset: int, - wrap_lines: bool, - ) -> None: - - self.window = window - self.ui_content = ui_content - self.vertical_scroll = vertical_scroll - self.window_width = window_width # Width without margins. - self.window_height = window_height - - self.configured_scroll_offsets = configured_scroll_offsets - self.visible_line_to_row_col = visible_line_to_row_col - self.wrap_lines = wrap_lines - - self._rowcol_to_yx = rowcol_to_yx # row/col from input to absolute y/x - # screen coordinates. - self._x_offset = x_offset - self._y_offset = y_offset - - @property - def visible_line_to_input_line(self) -> Dict[int, int]: - return { - visible_line: rowcol[0] - for visible_line, rowcol in self.visible_line_to_row_col.items() - } - - @property - def cursor_position(self) -> Point: - """ - Return the cursor position coordinates, relative to the left/top corner - of the rendered screen. - """ - cpos = self.ui_content.cursor_position - try: - y, x = self._rowcol_to_yx[cpos.y, cpos.x] - except KeyError: - # For `DummyControl` for instance, the content can be empty, and so - # will `_rowcol_to_yx` be. Return 0/0 by default. - return Point(x=0, y=0) - else: - return Point(x=x - self._x_offset, y=y - self._y_offset) - - @property - def applied_scroll_offsets(self) -> "ScrollOffsets": - """ - Return a :class:`.ScrollOffsets` instance that indicates the actual - offset. This can be less than or equal to what's configured. E.g, when - the cursor is completely at the top, the top offset will be zero rather - than what's configured. - """ - if self.displayed_lines[0] == 0: - top = 0 - else: - # Get row where the cursor is displayed. - y = self.input_line_to_visible_line[self.ui_content.cursor_position.y] - top = min(y, self.configured_scroll_offsets.top) - - return ScrollOffsets( - top=top, - bottom=min( - self.ui_content.line_count - self.displayed_lines[-1] - 1, - self.configured_scroll_offsets.bottom, - ), - # For left/right, it probably doesn't make sense to return something. - # (We would have to calculate the widths of all the lines and keep - # double width characters in mind.) - left=0, - right=0, - ) - - @property - def displayed_lines(self) -> List[int]: - """ - List of all the visible rows. (Line numbers of the input buffer.) - The last line may not be entirely visible. - """ - return sorted(row for row, col in self.visible_line_to_row_col.values()) - - @property - def input_line_to_visible_line(self) -> Dict[int, int]: - """ - Return the dictionary mapping the line numbers of the input buffer to - the lines of the screen. When a line spans several rows at the screen, - the first row appears in the dictionary. - """ - result: Dict[int, int] = {} - for k, v in self.visible_line_to_input_line.items(): - if v in result: - result[v] = min(result[v], k) - else: - result[v] = k - return result - - def first_visible_line(self, after_scroll_offset: bool = False) -> int: - """ - Return the line number (0 based) of the input document that corresponds - with the first visible line. - """ - if after_scroll_offset: - return self.displayed_lines[self.applied_scroll_offsets.top] - else: - return self.displayed_lines[0] - - def last_visible_line(self, before_scroll_offset: bool = False) -> int: - """ - Like `first_visible_line`, but for the last visible line. - """ - if before_scroll_offset: - return self.displayed_lines[-1 - self.applied_scroll_offsets.bottom] - else: - return self.displayed_lines[-1] - - def center_visible_line( - self, before_scroll_offset: bool = False, after_scroll_offset: bool = False - ) -> int: - """ - Like `first_visible_line`, but for the center visible line. - """ - return ( - self.first_visible_line(after_scroll_offset) - + ( - self.last_visible_line(before_scroll_offset) - - self.first_visible_line(after_scroll_offset) - ) - // 2 - ) - - @property - def content_height(self) -> int: - """ - The full height of the user control. - """ - return self.ui_content.line_count - - @property - def full_height_visible(self) -> bool: - """ - True when the full height is visible (There is no vertical scroll.) - """ - return ( - self.vertical_scroll == 0 - and self.last_visible_line() == self.content_height - ) - - @property - def top_visible(self) -> bool: - """ - True when the top of the buffer is visible. - """ - return self.vertical_scroll == 0 - - @property - def bottom_visible(self) -> bool: - """ - True when the bottom of the buffer is visible. - """ - return self.last_visible_line() == self.content_height - 1 - - @property - def vertical_scroll_percentage(self) -> int: - """ - Vertical scroll as a percentage. (0 means: the top is visible, - 100 means: the bottom is visible.) - """ - if self.bottom_visible: - return 100 - else: - return 100 * self.vertical_scroll // self.content_height - - def get_height_for_line(self, lineno: int) -> int: - """ - Return the height of the given line. - (The height that it would take, if this line became visible.) - """ - if self.wrap_lines: - return self.ui_content.get_height_for_line( - lineno, self.window_width, self.window.get_line_prefix - ) - else: - return 1 - - -class ScrollOffsets: - """ - Scroll offsets for the :class:`.Window` class. - - Note that left/right offsets only make sense if line wrapping is disabled. - """ - - def __init__( - self, - top: Union[int, Callable[[], int]] = 0, - bottom: Union[int, Callable[[], int]] = 0, - left: Union[int, Callable[[], int]] = 0, - right: Union[int, Callable[[], int]] = 0, - ) -> None: - - self._top = top - self._bottom = bottom - self._left = left - self._right = right - - @property - def top(self) -> int: - return to_int(self._top) - - @property - def bottom(self) -> int: - return to_int(self._bottom) - - @property - def left(self) -> int: - return to_int(self._left) - - @property - def right(self) -> int: - return to_int(self._right) - - def __repr__(self) -> str: - return "ScrollOffsets(top=%r, bottom=%r, left=%r, right=%r)" % ( - self._top, - self._bottom, - self._left, - self._right, - ) - - -class ColorColumn: - """ - Column for a :class:`.Window` to be colored. - """ - - def __init__(self, position: int, style: str = "class:color-column") -> None: - self.position = position - self.style = style - - -_in_insert_mode = vi_insert_mode | emacs_insert_mode - - -class WindowAlign(Enum): - """ - Alignment of the Window content. - - Note that this is different from `HorizontalAlign` and `VerticalAlign`, - which are used for the alignment of the child containers in respectively - `VSplit` and `HSplit`. - """ - - LEFT = "LEFT" - RIGHT = "RIGHT" - CENTER = "CENTER" - - -class Window(Container): - """ - Container that holds a control. - - :param content: :class:`.UIControl` instance. - :param width: :class:`.Dimension` instance or callable. - :param height: :class:`.Dimension` instance or callable. - :param z_index: When specified, this can be used to bring element in front - of floating elements. - :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 ignore_content_width: A `bool` or :class:`.Filter` instance. Ignore - the :class:`.UIContent` width when calculating the dimensions. - :param ignore_content_height: A `bool` or :class:`.Filter` instance. Ignore - the :class:`.UIContent` height when calculating the dimensions. - :param left_margins: A list of :class:`.Margin` instance to be displayed on - the left. For instance: :class:`~prompt_toolkit.layout.NumberedMargin` - can be one of them in order to show line numbers. - :param right_margins: Like `left_margins`, but on the other side. - :param scroll_offsets: :class:`.ScrollOffsets` instance, representing the - preferred amount of lines/columns to be always visible before/after the - cursor. When both top and bottom are a very high number, the cursor - will be centered vertically most of the time. - :param allow_scroll_beyond_bottom: A `bool` or - :class:`.Filter` instance. When True, allow scrolling so far, that the - top part of the content is not visible anymore, while there is still - empty space available at the bottom of the window. In the Vi editor for - instance, this is possible. You will see tildes while the top part of - the body is hidden. - :param wrap_lines: A `bool` or :class:`.Filter` instance. When True, don't - scroll horizontally, but wrap lines instead. - :param get_vertical_scroll: Callable that takes this window - instance as input and returns a preferred vertical scroll. - (When this is `None`, the scroll is only determined by the last and - current cursor position.) - :param get_horizontal_scroll: Callable that takes this window - instance as input and returns a preferred vertical scroll. - :param always_hide_cursor: A `bool` or - :class:`.Filter` instance. When True, never display the cursor, even - when the user control specifies a cursor position. - :param cursorline: A `bool` or :class:`.Filter` instance. When True, - display a cursorline. - :param cursorcolumn: A `bool` or :class:`.Filter` instance. When True, - display a cursorcolumn. - :param colorcolumns: A list of :class:`.ColorColumn` instances that - describe the columns to be highlighted, or a callable that returns such - a list. - :param align: :class:`.WindowAlign` value or callable that returns an - :class:`.WindowAlign` value. alignment of content. - :param style: A style string. Style to be applied to all the cells in this - window. (This can be a callable that returns a string.) - :param char: (string) Character to be used for filling the background. This - can also be a callable that returns a character. - :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. - """ - - def __init__( - self, - content: Optional[UIControl] = None, - width: AnyDimension = None, - height: AnyDimension = None, - z_index: Optional[int] = None, - dont_extend_width: FilterOrBool = False, - dont_extend_height: FilterOrBool = False, - ignore_content_width: FilterOrBool = False, - ignore_content_height: FilterOrBool = False, - left_margins: Optional[Sequence[Margin]] = None, - right_margins: Optional[Sequence[Margin]] = None, - scroll_offsets: Optional[ScrollOffsets] = None, - allow_scroll_beyond_bottom: FilterOrBool = False, - wrap_lines: FilterOrBool = False, - get_vertical_scroll: Optional[Callable[["Window"], int]] = None, - get_horizontal_scroll: Optional[Callable[["Window"], int]] = None, - always_hide_cursor: FilterOrBool = False, - cursorline: FilterOrBool = False, - cursorcolumn: FilterOrBool = False, - colorcolumns: Union[ - None, List[ColorColumn], Callable[[], List[ColorColumn]] - ] = None, - align: Union[WindowAlign, Callable[[], WindowAlign]] = WindowAlign.LEFT, - style: Union[str, Callable[[], str]] = "", - char: Union[None, str, Callable[[], str]] = None, - get_line_prefix: Optional[GetLinePrefixCallable] = None, - ) -> None: - - self.allow_scroll_beyond_bottom = to_filter(allow_scroll_beyond_bottom) - self.always_hide_cursor = to_filter(always_hide_cursor) - self.wrap_lines = to_filter(wrap_lines) - self.cursorline = to_filter(cursorline) - self.cursorcolumn = to_filter(cursorcolumn) - - self.content = content or DummyControl() - self.dont_extend_width = to_filter(dont_extend_width) - self.dont_extend_height = to_filter(dont_extend_height) - self.ignore_content_width = to_filter(ignore_content_width) - self.ignore_content_height = to_filter(ignore_content_height) - self.left_margins = left_margins or [] - self.right_margins = right_margins or [] - self.scroll_offsets = scroll_offsets or ScrollOffsets() - self.get_vertical_scroll = get_vertical_scroll - self.get_horizontal_scroll = get_horizontal_scroll - self.colorcolumns = colorcolumns or [] - self.align = align - self.style = style - self.char = char - self.get_line_prefix = get_line_prefix - - self.width = width - self.height = height - self.z_index = z_index - - # Cache for the screens generated by the margin. - self._ui_content_cache: SimpleCache[ - Tuple[int, int, int], UIContent - ] = SimpleCache(maxsize=8) - self._margin_width_cache: SimpleCache[Tuple[Margin, int], int] = SimpleCache( - maxsize=1 - ) - - self.reset() - - def __repr__(self) -> str: - return "Window(content=%r)" % self.content - - def reset(self) -> None: - self.content.reset() - - #: Scrolling position of the main content. - self.vertical_scroll = 0 - self.horizontal_scroll = 0 - - # Vertical scroll 2: this is the vertical offset that a line is - # scrolled if a single line (the one that contains the cursor) consumes - # all of the vertical space. - self.vertical_scroll_2 = 0 - - #: Keep render information (mappings between buffer input and render - #: output.) - self.render_info: Optional[WindowRenderInfo] = None - - def _get_margin_width(self, margin: Margin) -> int: - """ - Return the width for this margin. - (Calculate only once per render time.) - """ - # Margin.get_width, needs to have a UIContent instance. - def get_ui_content() -> UIContent: - return self._get_ui_content(width=0, height=0) - - def get_width() -> int: - return margin.get_width(get_ui_content) - - key = (margin, get_app().render_counter) - return self._margin_width_cache.get(key, get_width) - - def _get_total_margin_width(self) -> int: - """ - Calculate and return the width of the margin (left + right). - """ - return sum(self._get_margin_width(m) for m in self.left_margins) + sum( - self._get_margin_width(m) for m in self.right_margins - ) - - def preferred_width(self, max_available_width: int) -> Dimension: - """ - Calculate the preferred width for this window. - """ - - def preferred_content_width() -> Optional[int]: - """Content width: is only calculated if no exact width for the - window was given.""" - if self.ignore_content_width(): - return None - - # Calculate the width of the margin. - total_margin_width = self._get_total_margin_width() - - # Window of the content. (Can be `None`.) - preferred_width = self.content.preferred_width( - max_available_width - total_margin_width - ) - - if preferred_width is not None: - # Include width of the margins. - preferred_width += total_margin_width - return preferred_width - - # Merge. - return self._merge_dimensions( - dimension=to_dimension(self.width), - get_preferred=preferred_content_width, - dont_extend=self.dont_extend_width(), - ) - - def preferred_height(self, width: int, max_available_height: int) -> Dimension: - """ - Calculate the preferred height for this window. - """ - - def preferred_content_height() -> Optional[int]: - """Content height: is only calculated if no exact height for the - window was given.""" - if self.ignore_content_height(): - return None - - total_margin_width = self._get_total_margin_width() - wrap_lines = self.wrap_lines() - - return self.content.preferred_height( - width - total_margin_width, - max_available_height, - wrap_lines, - self.get_line_prefix, - ) - - return self._merge_dimensions( - dimension=to_dimension(self.height), - get_preferred=preferred_content_height, - dont_extend=self.dont_extend_height(), - ) - - @staticmethod - def _merge_dimensions( - dimension: Optional[Dimension], - get_preferred: Callable[[], Optional[int]], - dont_extend: bool = False, - ) -> Dimension: - """ - Take the Dimension from this `Window` class and the received preferred - size from the `UIControl` and return a `Dimension` to report to the - parent container. - """ - dimension = dimension or Dimension() - - # When a preferred dimension was explicitly given to the Window, - # ignore the UIControl. - preferred: Optional[int] - - if dimension.preferred_specified: - preferred = dimension.preferred - else: - # Otherwise, calculate the preferred dimension from the UI control - # content. - preferred = get_preferred() - - # When a 'preferred' dimension is given by the UIControl, make sure - # that it stays within the bounds of the Window. - if preferred is not None: - if dimension.max_specified: - preferred = min(preferred, dimension.max) - - if dimension.min_specified: - preferred = max(preferred, dimension.min) - - # When a `dont_extend` flag has been given, use the preferred dimension - # also as the max dimension. - max_: Optional[int] - min_: Optional[int] - - if dont_extend and preferred is not None: - max_ = min(dimension.max, preferred) - else: - max_ = dimension.max if dimension.max_specified else None - - min_ = dimension.min if dimension.min_specified else None - - return Dimension( - min=min_, max=max_, preferred=preferred, weight=dimension.weight - ) - - def _get_ui_content(self, width: int, height: int) -> UIContent: - """ - Create a `UIContent` instance. - """ - - def get_content() -> UIContent: - return self.content.create_content(width=width, height=height) - - key = (get_app().render_counter, width, height) - return self._ui_content_cache.get(key, get_content) - - def _get_digraph_char(self) -> Optional[str]: - "Return `False`, or the Digraph symbol to be used." - app = get_app() - if app.quoted_insert: - return "^" - if app.vi_state.waiting_for_digraph: - if app.vi_state.digraph_symbol1: - return app.vi_state.digraph_symbol1 - return "?" - return None - - def write_to_screen( - self, - screen: Screen, - mouse_handlers: MouseHandlers, - write_position: WritePosition, - parent_style: str, - erase_bg: bool, - z_index: Optional[int], - ) -> None: - """ - Write window to screen. This renders the user control, the margins and - copies everything over to the absolute position at the given screen. - """ - # If dont_extend_width/height was given. Then reduce width/height in - # WritePosition if the parent wanted us to paint in a bigger area. - # (This happens if this window is bundled with another window in a - # HSplit/VSplit, but with different size requirements.) - write_position = WritePosition( - xpos=write_position.xpos, - ypos=write_position.ypos, - width=write_position.width, - height=write_position.height, - ) - - if self.dont_extend_width(): - write_position.width = min( - write_position.width, - self.preferred_width(write_position.width).preferred, - ) - - if self.dont_extend_height(): - write_position.height = min( - write_position.height, - self.preferred_height( - write_position.width, write_position.height - ).preferred, - ) - - # Draw - z_index = z_index if self.z_index is None else self.z_index - - draw_func = partial( - self._write_to_screen_at_index, - screen, - mouse_handlers, - write_position, - parent_style, - erase_bg, - ) - - if z_index is None or z_index <= 0: - # When no z_index is given, draw right away. - draw_func() - else: - # Otherwise, postpone. - screen.draw_with_z_index(z_index=z_index, draw_func=draw_func) - - def _write_to_screen_at_index( - self, - screen: Screen, - mouse_handlers: MouseHandlers, - write_position: WritePosition, - parent_style: str, - erase_bg: bool, - ) -> None: - # Don't bother writing invisible windows. - # (We save some time, but also avoid applying last-line styling.) - if write_position.height <= 0 or write_position.width <= 0: - return - - # Calculate margin sizes. - left_margin_widths = [self._get_margin_width(m) for m in self.left_margins] - right_margin_widths = [self._get_margin_width(m) for m in self.right_margins] - total_margin_width = sum(left_margin_widths + right_margin_widths) - - # Render UserControl. - ui_content = self.content.create_content( - write_position.width - total_margin_width, write_position.height - ) - assert isinstance(ui_content, UIContent) - - # Scroll content. - wrap_lines = self.wrap_lines() - self._scroll( - ui_content, write_position.width - total_margin_width, write_position.height - ) - - # Erase background and fill with `char`. - self._fill_bg(screen, write_position, erase_bg) - - # Resolve `align` attribute. - align = self.align() if callable(self.align) else self.align - - # Write body - visible_line_to_row_col, rowcol_to_yx = self._copy_body( - ui_content, - screen, - write_position, - sum(left_margin_widths), - write_position.width - total_margin_width, - self.vertical_scroll, - self.horizontal_scroll, - wrap_lines=wrap_lines, - highlight_lines=True, - vertical_scroll_2=self.vertical_scroll_2, - always_hide_cursor=self.always_hide_cursor(), - has_focus=get_app().layout.current_control == self.content, - align=align, - get_line_prefix=self.get_line_prefix, - ) - - # Remember render info. (Set before generating the margins. They need this.) - x_offset = write_position.xpos + sum(left_margin_widths) - y_offset = write_position.ypos - - render_info = WindowRenderInfo( - window=self, - ui_content=ui_content, - horizontal_scroll=self.horizontal_scroll, - vertical_scroll=self.vertical_scroll, - window_width=write_position.width - total_margin_width, - window_height=write_position.height, - configured_scroll_offsets=self.scroll_offsets, - visible_line_to_row_col=visible_line_to_row_col, - rowcol_to_yx=rowcol_to_yx, - x_offset=x_offset, - y_offset=y_offset, - wrap_lines=wrap_lines, - ) - self.render_info = render_info - - # Set mouse handlers. - def mouse_handler(mouse_event: MouseEvent) -> "NotImplementedOrNone": - """ - Wrapper around the mouse_handler of the `UIControl` that turns - screen coordinates into line coordinates. - Returns `NotImplemented` if no UI invalidation should be done. - """ - # Don't handle mouse events outside of the current modal part of - # the UI. - if self not in get_app().layout.walk_through_modal_area(): - return NotImplemented - - # Find row/col position first. - yx_to_rowcol = {v: k for k, v in rowcol_to_yx.items()} - y = mouse_event.position.y - x = mouse_event.position.x - - # If clicked below the content area, look for a position in the - # last line instead. - max_y = write_position.ypos + len(visible_line_to_row_col) - 1 - y = min(max_y, y) - result: NotImplementedOrNone - - while x >= 0: - try: - row, col = yx_to_rowcol[y, x] - except KeyError: - # Try again. (When clicking on the right side of double - # width characters, or on the right side of the input.) - x -= 1 - else: - # Found position, call handler of UIControl. - result = self.content.mouse_handler( - MouseEvent( - position=Point(x=col, y=row), - event_type=mouse_event.event_type, - button=mouse_event.button, - modifiers=mouse_event.modifiers, - ) - ) - break - else: - # nobreak. - # (No x/y coordinate found for the content. This happens in - # case of a DummyControl, that does not have any content. - # Report (0,0) instead.) - result = self.content.mouse_handler( - MouseEvent( - position=Point(x=0, y=0), - event_type=mouse_event.event_type, - button=mouse_event.button, - modifiers=mouse_event.modifiers, - ) - ) - - # If it returns NotImplemented, handle it here. - if result == NotImplemented: - result = self._mouse_handler(mouse_event) - - return result - - mouse_handlers.set_mouse_handler_for_range( - x_min=write_position.xpos + sum(left_margin_widths), - x_max=write_position.xpos + write_position.width - total_margin_width, - y_min=write_position.ypos, - y_max=write_position.ypos + write_position.height, - handler=mouse_handler, - ) - - # Render and copy margins. - move_x = 0 - - def render_margin(m: Margin, width: int) -> UIContent: - "Render margin. Return `Screen`." - # Retrieve margin fragments. - fragments = m.create_margin(render_info, width, write_position.height) - - # Turn it into a UIContent object. - # already rendered those fragments using this size.) - return FormattedTextControl(fragments).create_content( - width + 1, write_position.height - ) - - for m, width in zip(self.left_margins, left_margin_widths): - if width > 0: # (ConditionalMargin returns a zero width. -- Don't render.) - # Create screen for margin. - margin_content = render_margin(m, width) - - # Copy and shift X. - self._copy_margin(margin_content, screen, write_position, move_x, width) - move_x += width - - move_x = write_position.width - sum(right_margin_widths) - - for m, width in zip(self.right_margins, right_margin_widths): - # Create screen for margin. - margin_content = render_margin(m, width) - - # Copy and shift X. - self._copy_margin(margin_content, screen, write_position, move_x, width) - move_x += width - - # Apply 'self.style' - self._apply_style(screen, write_position, parent_style) - - # Tell the screen that this user control has been painted at this - # position. - screen.visible_windows_to_write_positions[self] = write_position - - def _copy_body( - self, - ui_content: UIContent, - new_screen: Screen, - write_position: WritePosition, - move_x: int, - width: int, - vertical_scroll: int = 0, - horizontal_scroll: int = 0, - wrap_lines: bool = False, - highlight_lines: bool = False, - vertical_scroll_2: int = 0, - always_hide_cursor: bool = False, - has_focus: bool = False, - align: WindowAlign = WindowAlign.LEFT, - get_line_prefix: Optional[Callable[[int, int], AnyFormattedText]] = None, - ) -> Tuple[Dict[int, Tuple[int, int]], Dict[Tuple[int, int], Tuple[int, int]]]: - """ - Copy the UIContent into the output screen. - Return (visible_line_to_row_col, rowcol_to_yx) tuple. - - :param get_line_prefix: None or a callable that takes a line number - (int) and a wrap_count (int) and returns formatted text. - """ - xpos = write_position.xpos + move_x - ypos = write_position.ypos - line_count = ui_content.line_count - new_buffer = new_screen.data_buffer - empty_char = _CHAR_CACHE["", ""] - - # Map visible line number to (row, col) of input. - # 'col' will always be zero if line wrapping is off. - visible_line_to_row_col: Dict[int, Tuple[int, int]] = {} - - # Maps (row, col) from the input to (y, x) screen coordinates. - rowcol_to_yx: Dict[Tuple[int, int], Tuple[int, int]] = {} - - def copy_line( - line: StyleAndTextTuples, - lineno: int, - x: int, - y: int, - is_input: bool = False, - ) -> Tuple[int, int]: - """ - Copy over a single line to the output screen. This can wrap over - multiple lines in the output. It will call the prefix (prompt) - function before every line. - """ - if is_input: - current_rowcol_to_yx = rowcol_to_yx - else: - current_rowcol_to_yx = {} # Throwaway dictionary. - - # Draw line prefix. - if is_input and get_line_prefix: - prompt = to_formatted_text(get_line_prefix(lineno, 0)) - x, y = copy_line(prompt, lineno, x, y, is_input=False) - - # Scroll horizontally. - skipped = 0 # Characters skipped because of horizontal scrolling. - if horizontal_scroll and is_input: - h_scroll = horizontal_scroll - line = explode_text_fragments(line) - while h_scroll > 0 and line: - h_scroll -= get_cwidth(line[0][1]) - skipped += 1 - del line[:1] # Remove first character. - - x -= h_scroll # When scrolling over double width character, - # this can end up being negative. - - # Align this line. (Note that this doesn't work well when we use - # get_line_prefix and that function returns variable width prefixes.) - if align == WindowAlign.CENTER: - line_width = fragment_list_width(line) - if line_width < width: - x += (width - line_width) // 2 - elif align == WindowAlign.RIGHT: - line_width = fragment_list_width(line) - if line_width < width: - x += width - line_width - - col = 0 - wrap_count = 0 - for style, text, *_ in line: - new_buffer_row = new_buffer[y + ypos] - - # Remember raw VT escape sequences. (E.g. FinalTerm's - # escape sequences.) - if "[ZeroWidthEscape]" in style: - new_screen.zero_width_escapes[y + ypos][x + xpos] += text - continue - - for c in text: - char = _CHAR_CACHE[c, style] - char_width = char.width - - # Wrap when the line width is exceeded. - if wrap_lines and x + char_width > width: - visible_line_to_row_col[y + 1] = ( - lineno, - visible_line_to_row_col[y][1] + x, - ) - y += 1 - wrap_count += 1 - x = 0 - - # Insert line prefix (continuation prompt). - if is_input and get_line_prefix: - prompt = to_formatted_text( - get_line_prefix(lineno, wrap_count) - ) - x, y = copy_line(prompt, lineno, x, y, is_input=False) - - new_buffer_row = new_buffer[y + ypos] - - if y >= write_position.height: - return x, y # Break out of all for loops. - - # Set character in screen and shift 'x'. + ) # Draw as late as possible, but keep the order. + screen.draw_with_z_index( + z_index=new_z_index, + draw_func=partial( + self._draw_float, + fl, + screen, + mouse_handlers, + write_position, + style, + erase_bg, + new_z_index, + ), + ) + else: + self._draw_float( + fl, + screen, + mouse_handlers, + write_position, + style, + erase_bg, + new_z_index, + ) + + def _draw_float( + self, + fl: "Float", + screen: Screen, + mouse_handlers: MouseHandlers, + write_position: WritePosition, + style: str, + erase_bg: bool, + z_index: Optional[int], + ) -> None: + "Draw a single Float." + # When a menu_position was given, use this instead of the cursor + # position. (These cursor positions are absolute, translate again + # relative to the write_position.) + # Note: This should be inside the for-loop, because one float could + # set the cursor position to be used for the next one. + cpos = screen.get_menu_position( + fl.attach_to_window or get_app().layout.current_window + ) + cursor_position = Point( + x=cpos.x - write_position.xpos, y=cpos.y - write_position.ypos + ) + + fl_width = fl.get_width() + fl_height = fl.get_height() + width: int + height: int + xpos: int + ypos: int + + # Left & width given. + if fl.left is not None and fl_width is not None: + xpos = fl.left + width = fl_width + # Left & right given -> calculate width. + elif fl.left is not None and fl.right is not None: + xpos = fl.left + width = write_position.width - fl.left - fl.right + # Width & right given -> calculate left. + elif fl_width is not None and fl.right is not None: + xpos = write_position.width - fl.right - fl_width + width = fl_width + # Near x position of cursor. + elif fl.xcursor: + if fl_width is None: + width = fl.content.preferred_width(write_position.width).preferred + width = min(write_position.width, width) + else: + width = fl_width + + xpos = cursor_position.x + if xpos + width > write_position.width: + xpos = max(0, write_position.width - width) + # Only width given -> center horizontally. + elif fl_width: + xpos = int((write_position.width - fl_width) / 2) + width = fl_width + # Otherwise, take preferred width from float content. + else: + width = fl.content.preferred_width(write_position.width).preferred + + if fl.left is not None: + xpos = fl.left + elif fl.right is not None: + xpos = max(0, write_position.width - width - fl.right) + else: # Center horizontally. + xpos = max(0, int((write_position.width - width) / 2)) + + # Trim. + width = min(width, write_position.width - xpos) + + # Top & height given. + if fl.top is not None and fl_height is not None: + ypos = fl.top + height = fl_height + # Top & bottom given -> calculate height. + elif fl.top is not None and fl.bottom is not None: + ypos = fl.top + height = write_position.height - fl.top - fl.bottom + # Height & bottom given -> calculate top. + elif fl_height is not None and fl.bottom is not None: + ypos = write_position.height - fl_height - fl.bottom + height = fl_height + # Near cursor. + elif fl.ycursor: + ypos = cursor_position.y + (0 if fl.allow_cover_cursor else 1) + + if fl_height is None: + height = fl.content.preferred_height( + width, write_position.height + ).preferred + else: + height = fl_height + + # Reduce height if not enough space. (We can use the height + # when the content requires it.) + if height > write_position.height - ypos: + if write_position.height - ypos + 1 >= ypos: + # When the space below the cursor is more than + # the space above, just reduce the height. + height = write_position.height - ypos + else: + # Otherwise, fit the float above the cursor. + height = min(height, cursor_position.y) + ypos = cursor_position.y - height + + # Only height given -> center vertically. + elif fl_height: + ypos = int((write_position.height - fl_height) / 2) + height = fl_height + # Otherwise, take preferred height from content. + else: + height = fl.content.preferred_height(width, write_position.height).preferred + + if fl.top is not None: + ypos = fl.top + elif fl.bottom is not None: + ypos = max(0, write_position.height - height - fl.bottom) + else: # Center vertically. + ypos = max(0, int((write_position.height - height) / 2)) + + # Trim. + height = min(height, write_position.height - ypos) + + # Write float. + # (xpos and ypos can be negative: a float can be partially visible.) + if height > 0 and width > 0: + wp = WritePosition( + xpos=xpos + write_position.xpos, + ypos=ypos + write_position.ypos, + width=width, + height=height, + ) + + if not fl.hide_when_covering_content or self._area_is_empty(screen, wp): + fl.content.write_to_screen( + screen, + mouse_handlers, + wp, + style, + erase_bg=not fl.transparent(), + z_index=z_index, + ) + + def _area_is_empty(self, screen: Screen, write_position: WritePosition) -> bool: + """ + Return True when the area below the write position is still empty. + (For floats that should not hide content underneath.) + """ + wp = write_position + + for y in range(wp.ypos, wp.ypos + wp.height): + if y in screen.data_buffer: + row = screen.data_buffer[y] + + for x in range(wp.xpos, wp.xpos + wp.width): + c = row[x] + if c.char != " ": + return False + + return True + + def is_modal(self) -> bool: + return self.modal + + def get_key_bindings(self) -> Optional[KeyBindingsBase]: + return self.key_bindings + + def get_children(self) -> List[Container]: + children = [self.content] + children.extend(f.content for f in self.floats) + return children + + +class Float: + """ + Float for use in a :class:`.FloatContainer`. + Except for the `content` parameter, all other options are optional. + + :param content: :class:`.Container` instance. + + :param width: :class:`.Dimension` or callable which returns a :class:`.Dimension`. + :param height: :class:`.Dimension` or callable which returns a :class:`.Dimension`. + + :param left: Distance to the left edge of the :class:`.FloatContainer`. + :param right: Distance to the right edge of the :class:`.FloatContainer`. + :param top: Distance to the top of the :class:`.FloatContainer`. + :param bottom: Distance to the bottom of the :class:`.FloatContainer`. + + :param attach_to_window: Attach to the cursor from this window, instead of + the current window. + :param hide_when_covering_content: Hide the float when it covers content underneath. + :param allow_cover_cursor: When `False`, make sure to display the float + below the cursor. Not on top of the indicated position. + :param z_index: Z-index position. For a Float, this needs to be at least + one. It is relative to the z_index of the parent container. + :param transparent: :class:`.Filter` indicating whether this float needs to be + drawn transparently. + """ + + def __init__( + self, + content: AnyContainer, + top: Optional[int] = None, + right: Optional[int] = None, + bottom: Optional[int] = None, + left: Optional[int] = None, + width: Optional[Union[int, Callable[[], int]]] = None, + height: Optional[Union[int, Callable[[], int]]] = None, + xcursor: bool = False, + ycursor: bool = False, + attach_to_window: Optional[AnyContainer] = None, + hide_when_covering_content: bool = False, + allow_cover_cursor: bool = False, + z_index: int = 1, + transparent: bool = False, + ) -> None: + + assert z_index >= 1 + + self.left = left + self.right = right + self.top = top + self.bottom = bottom + + self.width = width + self.height = height + + self.xcursor = xcursor + self.ycursor = ycursor + + self.attach_to_window = ( + to_window(attach_to_window) if attach_to_window else None + ) + + self.content = to_container(content) + self.hide_when_covering_content = hide_when_covering_content + self.allow_cover_cursor = allow_cover_cursor + self.z_index = z_index + self.transparent = to_filter(transparent) + + def get_width(self) -> Optional[int]: + if callable(self.width): + return self.width() + return self.width + + def get_height(self) -> Optional[int]: + if callable(self.height): + return self.height() + return self.height + + def __repr__(self) -> str: + return "Float(content=%r)" % self.content + + +class WindowRenderInfo: + """ + Render information for the last render time of this control. + It stores mapping information between the input buffers (in case of a + :class:`~prompt_toolkit.layout.controls.BufferControl`) and the actual + render position on the output screen. + + (Could be used for implementation of the Vi 'H' and 'L' key bindings as + well as implementing mouse support.) + + :param ui_content: The original :class:`.UIContent` instance that contains + the whole input, without clipping. (ui_content) + :param horizontal_scroll: The horizontal scroll of the :class:`.Window` instance. + :param vertical_scroll: The vertical scroll of the :class:`.Window` instance. + :param window_width: The width of the window that displays the content, + without the margins. + :param window_height: The height of the window that displays the content. + :param configured_scroll_offsets: The scroll offsets as configured for the + :class:`Window` instance. + :param visible_line_to_row_col: Mapping that maps the row numbers on the + displayed screen (starting from zero for the first visible line) to + (row, col) tuples pointing to the row and column of the :class:`.UIContent`. + :param rowcol_to_yx: Mapping that maps (row, column) tuples representing + coordinates of the :class:`UIContent` to (y, x) absolute coordinates at + the rendered screen. + """ + + def __init__( + self, + window: "Window", + ui_content: UIContent, + horizontal_scroll: int, + vertical_scroll: int, + window_width: int, + window_height: int, + configured_scroll_offsets: "ScrollOffsets", + visible_line_to_row_col: Dict[int, Tuple[int, int]], + rowcol_to_yx: Dict[Tuple[int, int], Tuple[int, int]], + x_offset: int, + y_offset: int, + wrap_lines: bool, + ) -> None: + + self.window = window + self.ui_content = ui_content + self.vertical_scroll = vertical_scroll + self.window_width = window_width # Width without margins. + self.window_height = window_height + + self.configured_scroll_offsets = configured_scroll_offsets + self.visible_line_to_row_col = visible_line_to_row_col + self.wrap_lines = wrap_lines + + self._rowcol_to_yx = rowcol_to_yx # row/col from input to absolute y/x + # screen coordinates. + self._x_offset = x_offset + self._y_offset = y_offset + + @property + def visible_line_to_input_line(self) -> Dict[int, int]: + return { + visible_line: rowcol[0] + for visible_line, rowcol in self.visible_line_to_row_col.items() + } + + @property + def cursor_position(self) -> Point: + """ + Return the cursor position coordinates, relative to the left/top corner + of the rendered screen. + """ + cpos = self.ui_content.cursor_position + try: + y, x = self._rowcol_to_yx[cpos.y, cpos.x] + except KeyError: + # For `DummyControl` for instance, the content can be empty, and so + # will `_rowcol_to_yx` be. Return 0/0 by default. + return Point(x=0, y=0) + else: + return Point(x=x - self._x_offset, y=y - self._y_offset) + + @property + def applied_scroll_offsets(self) -> "ScrollOffsets": + """ + Return a :class:`.ScrollOffsets` instance that indicates the actual + offset. This can be less than or equal to what's configured. E.g, when + the cursor is completely at the top, the top offset will be zero rather + than what's configured. + """ + if self.displayed_lines[0] == 0: + top = 0 + else: + # Get row where the cursor is displayed. + y = self.input_line_to_visible_line[self.ui_content.cursor_position.y] + top = min(y, self.configured_scroll_offsets.top) + + return ScrollOffsets( + top=top, + bottom=min( + self.ui_content.line_count - self.displayed_lines[-1] - 1, + self.configured_scroll_offsets.bottom, + ), + # For left/right, it probably doesn't make sense to return something. + # (We would have to calculate the widths of all the lines and keep + # double width characters in mind.) + left=0, + right=0, + ) + + @property + def displayed_lines(self) -> List[int]: + """ + List of all the visible rows. (Line numbers of the input buffer.) + The last line may not be entirely visible. + """ + return sorted(row for row, col in self.visible_line_to_row_col.values()) + + @property + def input_line_to_visible_line(self) -> Dict[int, int]: + """ + Return the dictionary mapping the line numbers of the input buffer to + the lines of the screen. When a line spans several rows at the screen, + the first row appears in the dictionary. + """ + result: Dict[int, int] = {} + for k, v in self.visible_line_to_input_line.items(): + if v in result: + result[v] = min(result[v], k) + else: + result[v] = k + return result + + def first_visible_line(self, after_scroll_offset: bool = False) -> int: + """ + Return the line number (0 based) of the input document that corresponds + with the first visible line. + """ + if after_scroll_offset: + return self.displayed_lines[self.applied_scroll_offsets.top] + else: + return self.displayed_lines[0] + + def last_visible_line(self, before_scroll_offset: bool = False) -> int: + """ + Like `first_visible_line`, but for the last visible line. + """ + if before_scroll_offset: + return self.displayed_lines[-1 - self.applied_scroll_offsets.bottom] + else: + return self.displayed_lines[-1] + + def center_visible_line( + self, before_scroll_offset: bool = False, after_scroll_offset: bool = False + ) -> int: + """ + Like `first_visible_line`, but for the center visible line. + """ + return ( + self.first_visible_line(after_scroll_offset) + + ( + self.last_visible_line(before_scroll_offset) + - self.first_visible_line(after_scroll_offset) + ) + // 2 + ) + + @property + def content_height(self) -> int: + """ + The full height of the user control. + """ + return self.ui_content.line_count + + @property + def full_height_visible(self) -> bool: + """ + True when the full height is visible (There is no vertical scroll.) + """ + return ( + self.vertical_scroll == 0 + and self.last_visible_line() == self.content_height + ) + + @property + def top_visible(self) -> bool: + """ + True when the top of the buffer is visible. + """ + return self.vertical_scroll == 0 + + @property + def bottom_visible(self) -> bool: + """ + True when the bottom of the buffer is visible. + """ + return self.last_visible_line() == self.content_height - 1 + + @property + def vertical_scroll_percentage(self) -> int: + """ + Vertical scroll as a percentage. (0 means: the top is visible, + 100 means: the bottom is visible.) + """ + if self.bottom_visible: + return 100 + else: + return 100 * self.vertical_scroll // self.content_height + + def get_height_for_line(self, lineno: int) -> int: + """ + Return the height of the given line. + (The height that it would take, if this line became visible.) + """ + if self.wrap_lines: + return self.ui_content.get_height_for_line( + lineno, self.window_width, self.window.get_line_prefix + ) + else: + return 1 + + +class ScrollOffsets: + """ + Scroll offsets for the :class:`.Window` class. + + Note that left/right offsets only make sense if line wrapping is disabled. + """ + + def __init__( + self, + top: Union[int, Callable[[], int]] = 0, + bottom: Union[int, Callable[[], int]] = 0, + left: Union[int, Callable[[], int]] = 0, + right: Union[int, Callable[[], int]] = 0, + ) -> None: + + self._top = top + self._bottom = bottom + self._left = left + self._right = right + + @property + def top(self) -> int: + return to_int(self._top) + + @property + def bottom(self) -> int: + return to_int(self._bottom) + + @property + def left(self) -> int: + return to_int(self._left) + + @property + def right(self) -> int: + return to_int(self._right) + + def __repr__(self) -> str: + return "ScrollOffsets(top=%r, bottom=%r, left=%r, right=%r)" % ( + self._top, + self._bottom, + self._left, + self._right, + ) + + +class ColorColumn: + """ + Column for a :class:`.Window` to be colored. + """ + + def __init__(self, position: int, style: str = "class:color-column") -> None: + self.position = position + self.style = style + + +_in_insert_mode = vi_insert_mode | emacs_insert_mode + + +class WindowAlign(Enum): + """ + Alignment of the Window content. + + Note that this is different from `HorizontalAlign` and `VerticalAlign`, + which are used for the alignment of the child containers in respectively + `VSplit` and `HSplit`. + """ + + LEFT = "LEFT" + RIGHT = "RIGHT" + CENTER = "CENTER" + + +class Window(Container): + """ + Container that holds a control. + + :param content: :class:`.UIControl` instance. + :param width: :class:`.Dimension` instance or callable. + :param height: :class:`.Dimension` instance or callable. + :param z_index: When specified, this can be used to bring element in front + of floating elements. + :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 ignore_content_width: A `bool` or :class:`.Filter` instance. Ignore + the :class:`.UIContent` width when calculating the dimensions. + :param ignore_content_height: A `bool` or :class:`.Filter` instance. Ignore + the :class:`.UIContent` height when calculating the dimensions. + :param left_margins: A list of :class:`.Margin` instance to be displayed on + the left. For instance: :class:`~prompt_toolkit.layout.NumberedMargin` + can be one of them in order to show line numbers. + :param right_margins: Like `left_margins`, but on the other side. + :param scroll_offsets: :class:`.ScrollOffsets` instance, representing the + preferred amount of lines/columns to be always visible before/after the + cursor. When both top and bottom are a very high number, the cursor + will be centered vertically most of the time. + :param allow_scroll_beyond_bottom: A `bool` or + :class:`.Filter` instance. When True, allow scrolling so far, that the + top part of the content is not visible anymore, while there is still + empty space available at the bottom of the window. In the Vi editor for + instance, this is possible. You will see tildes while the top part of + the body is hidden. + :param wrap_lines: A `bool` or :class:`.Filter` instance. When True, don't + scroll horizontally, but wrap lines instead. + :param get_vertical_scroll: Callable that takes this window + instance as input and returns a preferred vertical scroll. + (When this is `None`, the scroll is only determined by the last and + current cursor position.) + :param get_horizontal_scroll: Callable that takes this window + instance as input and returns a preferred vertical scroll. + :param always_hide_cursor: A `bool` or + :class:`.Filter` instance. When True, never display the cursor, even + when the user control specifies a cursor position. + :param cursorline: A `bool` or :class:`.Filter` instance. When True, + display a cursorline. + :param cursorcolumn: A `bool` or :class:`.Filter` instance. When True, + display a cursorcolumn. + :param colorcolumns: A list of :class:`.ColorColumn` instances that + describe the columns to be highlighted, or a callable that returns such + a list. + :param align: :class:`.WindowAlign` value or callable that returns an + :class:`.WindowAlign` value. alignment of content. + :param style: A style string. Style to be applied to all the cells in this + window. (This can be a callable that returns a string.) + :param char: (string) Character to be used for filling the background. This + can also be a callable that returns a character. + :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. + """ + + def __init__( + self, + content: Optional[UIControl] = None, + width: AnyDimension = None, + height: AnyDimension = None, + z_index: Optional[int] = None, + dont_extend_width: FilterOrBool = False, + dont_extend_height: FilterOrBool = False, + ignore_content_width: FilterOrBool = False, + ignore_content_height: FilterOrBool = False, + left_margins: Optional[Sequence[Margin]] = None, + right_margins: Optional[Sequence[Margin]] = None, + scroll_offsets: Optional[ScrollOffsets] = None, + allow_scroll_beyond_bottom: FilterOrBool = False, + wrap_lines: FilterOrBool = False, + get_vertical_scroll: Optional[Callable[["Window"], int]] = None, + get_horizontal_scroll: Optional[Callable[["Window"], int]] = None, + always_hide_cursor: FilterOrBool = False, + cursorline: FilterOrBool = False, + cursorcolumn: FilterOrBool = False, + colorcolumns: Union[ + None, List[ColorColumn], Callable[[], List[ColorColumn]] + ] = None, + align: Union[WindowAlign, Callable[[], WindowAlign]] = WindowAlign.LEFT, + style: Union[str, Callable[[], str]] = "", + char: Union[None, str, Callable[[], str]] = None, + get_line_prefix: Optional[GetLinePrefixCallable] = None, + ) -> None: + + self.allow_scroll_beyond_bottom = to_filter(allow_scroll_beyond_bottom) + self.always_hide_cursor = to_filter(always_hide_cursor) + self.wrap_lines = to_filter(wrap_lines) + self.cursorline = to_filter(cursorline) + self.cursorcolumn = to_filter(cursorcolumn) + + self.content = content or DummyControl() + self.dont_extend_width = to_filter(dont_extend_width) + self.dont_extend_height = to_filter(dont_extend_height) + self.ignore_content_width = to_filter(ignore_content_width) + self.ignore_content_height = to_filter(ignore_content_height) + self.left_margins = left_margins or [] + self.right_margins = right_margins or [] + self.scroll_offsets = scroll_offsets or ScrollOffsets() + self.get_vertical_scroll = get_vertical_scroll + self.get_horizontal_scroll = get_horizontal_scroll + self.colorcolumns = colorcolumns or [] + self.align = align + self.style = style + self.char = char + self.get_line_prefix = get_line_prefix + + self.width = width + self.height = height + self.z_index = z_index + + # Cache for the screens generated by the margin. + self._ui_content_cache: SimpleCache[ + Tuple[int, int, int], UIContent + ] = SimpleCache(maxsize=8) + self._margin_width_cache: SimpleCache[Tuple[Margin, int], int] = SimpleCache( + maxsize=1 + ) + + self.reset() + + def __repr__(self) -> str: + return "Window(content=%r)" % self.content + + def reset(self) -> None: + self.content.reset() + + #: Scrolling position of the main content. + self.vertical_scroll = 0 + self.horizontal_scroll = 0 + + # Vertical scroll 2: this is the vertical offset that a line is + # scrolled if a single line (the one that contains the cursor) consumes + # all of the vertical space. + self.vertical_scroll_2 = 0 + + #: Keep render information (mappings between buffer input and render + #: output.) + self.render_info: Optional[WindowRenderInfo] = None + + def _get_margin_width(self, margin: Margin) -> int: + """ + Return the width for this margin. + (Calculate only once per render time.) + """ + # Margin.get_width, needs to have a UIContent instance. + def get_ui_content() -> UIContent: + return self._get_ui_content(width=0, height=0) + + def get_width() -> int: + return margin.get_width(get_ui_content) + + key = (margin, get_app().render_counter) + return self._margin_width_cache.get(key, get_width) + + def _get_total_margin_width(self) -> int: + """ + Calculate and return the width of the margin (left + right). + """ + return sum(self._get_margin_width(m) for m in self.left_margins) + sum( + self._get_margin_width(m) for m in self.right_margins + ) + + def preferred_width(self, max_available_width: int) -> Dimension: + """ + Calculate the preferred width for this window. + """ + + def preferred_content_width() -> Optional[int]: + """Content width: is only calculated if no exact width for the + window was given.""" + if self.ignore_content_width(): + return None + + # Calculate the width of the margin. + total_margin_width = self._get_total_margin_width() + + # Window of the content. (Can be `None`.) + preferred_width = self.content.preferred_width( + max_available_width - total_margin_width + ) + + if preferred_width is not None: + # Include width of the margins. + preferred_width += total_margin_width + return preferred_width + + # Merge. + return self._merge_dimensions( + dimension=to_dimension(self.width), + get_preferred=preferred_content_width, + dont_extend=self.dont_extend_width(), + ) + + def preferred_height(self, width: int, max_available_height: int) -> Dimension: + """ + Calculate the preferred height for this window. + """ + + def preferred_content_height() -> Optional[int]: + """Content height: is only calculated if no exact height for the + window was given.""" + if self.ignore_content_height(): + return None + + total_margin_width = self._get_total_margin_width() + wrap_lines = self.wrap_lines() + + return self.content.preferred_height( + width - total_margin_width, + max_available_height, + wrap_lines, + self.get_line_prefix, + ) + + return self._merge_dimensions( + dimension=to_dimension(self.height), + get_preferred=preferred_content_height, + dont_extend=self.dont_extend_height(), + ) + + @staticmethod + def _merge_dimensions( + dimension: Optional[Dimension], + get_preferred: Callable[[], Optional[int]], + dont_extend: bool = False, + ) -> Dimension: + """ + Take the Dimension from this `Window` class and the received preferred + size from the `UIControl` and return a `Dimension` to report to the + parent container. + """ + dimension = dimension or Dimension() + + # When a preferred dimension was explicitly given to the Window, + # ignore the UIControl. + preferred: Optional[int] + + if dimension.preferred_specified: + preferred = dimension.preferred + else: + # Otherwise, calculate the preferred dimension from the UI control + # content. + preferred = get_preferred() + + # When a 'preferred' dimension is given by the UIControl, make sure + # that it stays within the bounds of the Window. + if preferred is not None: + if dimension.max_specified: + preferred = min(preferred, dimension.max) + + if dimension.min_specified: + preferred = max(preferred, dimension.min) + + # When a `dont_extend` flag has been given, use the preferred dimension + # also as the max dimension. + max_: Optional[int] + min_: Optional[int] + + if dont_extend and preferred is not None: + max_ = min(dimension.max, preferred) + else: + max_ = dimension.max if dimension.max_specified else None + + min_ = dimension.min if dimension.min_specified else None + + return Dimension( + min=min_, max=max_, preferred=preferred, weight=dimension.weight + ) + + def _get_ui_content(self, width: int, height: int) -> UIContent: + """ + Create a `UIContent` instance. + """ + + def get_content() -> UIContent: + return self.content.create_content(width=width, height=height) + + key = (get_app().render_counter, width, height) + return self._ui_content_cache.get(key, get_content) + + def _get_digraph_char(self) -> Optional[str]: + "Return `False`, or the Digraph symbol to be used." + app = get_app() + if app.quoted_insert: + return "^" + if app.vi_state.waiting_for_digraph: + if app.vi_state.digraph_symbol1: + return app.vi_state.digraph_symbol1 + return "?" + return None + + def write_to_screen( + self, + screen: Screen, + mouse_handlers: MouseHandlers, + write_position: WritePosition, + parent_style: str, + erase_bg: bool, + z_index: Optional[int], + ) -> None: + """ + Write window to screen. This renders the user control, the margins and + copies everything over to the absolute position at the given screen. + """ + # If dont_extend_width/height was given. Then reduce width/height in + # WritePosition if the parent wanted us to paint in a bigger area. + # (This happens if this window is bundled with another window in a + # HSplit/VSplit, but with different size requirements.) + write_position = WritePosition( + xpos=write_position.xpos, + ypos=write_position.ypos, + width=write_position.width, + height=write_position.height, + ) + + if self.dont_extend_width(): + write_position.width = min( + write_position.width, + self.preferred_width(write_position.width).preferred, + ) + + if self.dont_extend_height(): + write_position.height = min( + write_position.height, + self.preferred_height( + write_position.width, write_position.height + ).preferred, + ) + + # Draw + z_index = z_index if self.z_index is None else self.z_index + + draw_func = partial( + self._write_to_screen_at_index, + screen, + mouse_handlers, + write_position, + parent_style, + erase_bg, + ) + + if z_index is None or z_index <= 0: + # When no z_index is given, draw right away. + draw_func() + else: + # Otherwise, postpone. + screen.draw_with_z_index(z_index=z_index, draw_func=draw_func) + + def _write_to_screen_at_index( + self, + screen: Screen, + mouse_handlers: MouseHandlers, + write_position: WritePosition, + parent_style: str, + erase_bg: bool, + ) -> None: + # Don't bother writing invisible windows. + # (We save some time, but also avoid applying last-line styling.) + if write_position.height <= 0 or write_position.width <= 0: + return + + # Calculate margin sizes. + left_margin_widths = [self._get_margin_width(m) for m in self.left_margins] + right_margin_widths = [self._get_margin_width(m) for m in self.right_margins] + total_margin_width = sum(left_margin_widths + right_margin_widths) + + # Render UserControl. + ui_content = self.content.create_content( + write_position.width - total_margin_width, write_position.height + ) + assert isinstance(ui_content, UIContent) + + # Scroll content. + wrap_lines = self.wrap_lines() + self._scroll( + ui_content, write_position.width - total_margin_width, write_position.height + ) + + # Erase background and fill with `char`. + self._fill_bg(screen, write_position, erase_bg) + + # Resolve `align` attribute. + align = self.align() if callable(self.align) else self.align + + # Write body + visible_line_to_row_col, rowcol_to_yx = self._copy_body( + ui_content, + screen, + write_position, + sum(left_margin_widths), + write_position.width - total_margin_width, + self.vertical_scroll, + self.horizontal_scroll, + wrap_lines=wrap_lines, + highlight_lines=True, + vertical_scroll_2=self.vertical_scroll_2, + always_hide_cursor=self.always_hide_cursor(), + has_focus=get_app().layout.current_control == self.content, + align=align, + get_line_prefix=self.get_line_prefix, + ) + + # Remember render info. (Set before generating the margins. They need this.) + x_offset = write_position.xpos + sum(left_margin_widths) + y_offset = write_position.ypos + + render_info = WindowRenderInfo( + window=self, + ui_content=ui_content, + horizontal_scroll=self.horizontal_scroll, + vertical_scroll=self.vertical_scroll, + window_width=write_position.width - total_margin_width, + window_height=write_position.height, + configured_scroll_offsets=self.scroll_offsets, + visible_line_to_row_col=visible_line_to_row_col, + rowcol_to_yx=rowcol_to_yx, + x_offset=x_offset, + y_offset=y_offset, + wrap_lines=wrap_lines, + ) + self.render_info = render_info + + # Set mouse handlers. + def mouse_handler(mouse_event: MouseEvent) -> "NotImplementedOrNone": + """ + Wrapper around the mouse_handler of the `UIControl` that turns + screen coordinates into line coordinates. + Returns `NotImplemented` if no UI invalidation should be done. + """ + # Don't handle mouse events outside of the current modal part of + # the UI. + if self not in get_app().layout.walk_through_modal_area(): + return NotImplemented + + # Find row/col position first. + yx_to_rowcol = {v: k for k, v in rowcol_to_yx.items()} + y = mouse_event.position.y + x = mouse_event.position.x + + # If clicked below the content area, look for a position in the + # last line instead. + max_y = write_position.ypos + len(visible_line_to_row_col) - 1 + y = min(max_y, y) + result: NotImplementedOrNone + + while x >= 0: + try: + row, col = yx_to_rowcol[y, x] + except KeyError: + # Try again. (When clicking on the right side of double + # width characters, or on the right side of the input.) + x -= 1 + else: + # Found position, call handler of UIControl. + result = self.content.mouse_handler( + MouseEvent( + position=Point(x=col, y=row), + event_type=mouse_event.event_type, + button=mouse_event.button, + modifiers=mouse_event.modifiers, + ) + ) + break + else: + # nobreak. + # (No x/y coordinate found for the content. This happens in + # case of a DummyControl, that does not have any content. + # Report (0,0) instead.) + result = self.content.mouse_handler( + MouseEvent( + position=Point(x=0, y=0), + event_type=mouse_event.event_type, + button=mouse_event.button, + modifiers=mouse_event.modifiers, + ) + ) + + # If it returns NotImplemented, handle it here. + if result == NotImplemented: + result = self._mouse_handler(mouse_event) + + return result + + mouse_handlers.set_mouse_handler_for_range( + x_min=write_position.xpos + sum(left_margin_widths), + x_max=write_position.xpos + write_position.width - total_margin_width, + y_min=write_position.ypos, + y_max=write_position.ypos + write_position.height, + handler=mouse_handler, + ) + + # Render and copy margins. + move_x = 0 + + def render_margin(m: Margin, width: int) -> UIContent: + "Render margin. Return `Screen`." + # Retrieve margin fragments. + fragments = m.create_margin(render_info, width, write_position.height) + + # Turn it into a UIContent object. + # already rendered those fragments using this size.) + return FormattedTextControl(fragments).create_content( + width + 1, write_position.height + ) + + for m, width in zip(self.left_margins, left_margin_widths): + if width > 0: # (ConditionalMargin returns a zero width. -- Don't render.) + # Create screen for margin. + margin_content = render_margin(m, width) + + # Copy and shift X. + self._copy_margin(margin_content, screen, write_position, move_x, width) + move_x += width + + move_x = write_position.width - sum(right_margin_widths) + + for m, width in zip(self.right_margins, right_margin_widths): + # Create screen for margin. + margin_content = render_margin(m, width) + + # Copy and shift X. + self._copy_margin(margin_content, screen, write_position, move_x, width) + move_x += width + + # Apply 'self.style' + self._apply_style(screen, write_position, parent_style) + + # Tell the screen that this user control has been painted at this + # position. + screen.visible_windows_to_write_positions[self] = write_position + + def _copy_body( + self, + ui_content: UIContent, + new_screen: Screen, + write_position: WritePosition, + move_x: int, + width: int, + vertical_scroll: int = 0, + horizontal_scroll: int = 0, + wrap_lines: bool = False, + highlight_lines: bool = False, + vertical_scroll_2: int = 0, + always_hide_cursor: bool = False, + has_focus: bool = False, + align: WindowAlign = WindowAlign.LEFT, + get_line_prefix: Optional[Callable[[int, int], AnyFormattedText]] = None, + ) -> Tuple[Dict[int, Tuple[int, int]], Dict[Tuple[int, int], Tuple[int, int]]]: + """ + Copy the UIContent into the output screen. + Return (visible_line_to_row_col, rowcol_to_yx) tuple. + + :param get_line_prefix: None or a callable that takes a line number + (int) and a wrap_count (int) and returns formatted text. + """ + xpos = write_position.xpos + move_x + ypos = write_position.ypos + line_count = ui_content.line_count + new_buffer = new_screen.data_buffer + empty_char = _CHAR_CACHE["", ""] + + # Map visible line number to (row, col) of input. + # 'col' will always be zero if line wrapping is off. + visible_line_to_row_col: Dict[int, Tuple[int, int]] = {} + + # Maps (row, col) from the input to (y, x) screen coordinates. + rowcol_to_yx: Dict[Tuple[int, int], Tuple[int, int]] = {} + + def copy_line( + line: StyleAndTextTuples, + lineno: int, + x: int, + y: int, + is_input: bool = False, + ) -> Tuple[int, int]: + """ + Copy over a single line to the output screen. This can wrap over + multiple lines in the output. It will call the prefix (prompt) + function before every line. + """ + if is_input: + current_rowcol_to_yx = rowcol_to_yx + else: + current_rowcol_to_yx = {} # Throwaway dictionary. + + # Draw line prefix. + if is_input and get_line_prefix: + prompt = to_formatted_text(get_line_prefix(lineno, 0)) + x, y = copy_line(prompt, lineno, x, y, is_input=False) + + # Scroll horizontally. + skipped = 0 # Characters skipped because of horizontal scrolling. + if horizontal_scroll and is_input: + h_scroll = horizontal_scroll + line = explode_text_fragments(line) + while h_scroll > 0 and line: + h_scroll -= get_cwidth(line[0][1]) + skipped += 1 + del line[:1] # Remove first character. + + x -= h_scroll # When scrolling over double width character, + # this can end up being negative. + + # Align this line. (Note that this doesn't work well when we use + # get_line_prefix and that function returns variable width prefixes.) + if align == WindowAlign.CENTER: + line_width = fragment_list_width(line) + if line_width < width: + x += (width - line_width) // 2 + elif align == WindowAlign.RIGHT: + line_width = fragment_list_width(line) + if line_width < width: + x += width - line_width + + col = 0 + wrap_count = 0 + for style, text, *_ in line: + new_buffer_row = new_buffer[y + ypos] + + # Remember raw VT escape sequences. (E.g. FinalTerm's + # escape sequences.) + if "[ZeroWidthEscape]" in style: + new_screen.zero_width_escapes[y + ypos][x + xpos] += text + continue + + for c in text: + char = _CHAR_CACHE[c, style] + char_width = char.width + + # Wrap when the line width is exceeded. + if wrap_lines and x + char_width > width: + visible_line_to_row_col[y + 1] = ( + lineno, + visible_line_to_row_col[y][1] + x, + ) + y += 1 + wrap_count += 1 + x = 0 + + # Insert line prefix (continuation prompt). + if is_input and get_line_prefix: + prompt = to_formatted_text( + get_line_prefix(lineno, wrap_count) + ) + x, y = copy_line(prompt, lineno, x, y, is_input=False) + + new_buffer_row = new_buffer[y + ypos] + + if y >= write_position.height: + return x, y # Break out of all for loops. + + # Set character in screen and shift 'x'. if x >= 0 and y >= 0 and x < width: - new_buffer_row[x + xpos] = char - - # When we print a multi width character, make sure - # to erase the neighbours positions in the screen. - # (The empty string if different from everything, - # so next redraw this cell will repaint anyway.) - if char_width > 1: - for i in range(1, char_width): - new_buffer_row[x + xpos + i] = empty_char - - # If this is a zero width characters, then it's - # probably part of a decomposed unicode character. - # See: https://en.wikipedia.org/wiki/Unicode_equivalence - # Merge it in the previous cell. - elif char_width == 0: - # Handle all character widths. If the previous - # character is a multiwidth character, then - # merge it two positions back. - for pw in [2, 1]: # Previous character width. - if ( - x - pw >= 0 - and new_buffer_row[x + xpos - pw].width == pw - ): - prev_char = new_buffer_row[x + xpos - pw] - char2 = _CHAR_CACHE[ - prev_char.char + c, prev_char.style - ] - new_buffer_row[x + xpos - pw] = char2 - - # Keep track of write position for each character. - current_rowcol_to_yx[lineno, col + skipped] = ( - y + ypos, - x + xpos, - ) - - col += 1 - x += char_width - return x, y - - # Copy content. - def copy() -> int: - y = -vertical_scroll_2 - lineno = vertical_scroll - - while y < write_position.height and lineno < line_count: - # Take the next line and copy it in the real screen. - line = ui_content.get_line(lineno) - - visible_line_to_row_col[y] = (lineno, horizontal_scroll) - - # Copy margin and actual line. - x = 0 - x, y = copy_line(line, lineno, x, y, is_input=True) - - lineno += 1 - y += 1 - return y - - copy() - - def cursor_pos_to_screen_pos(row: int, col: int) -> Point: - "Translate row/col from UIContent to real Screen coordinates." - try: - y, x = rowcol_to_yx[row, col] - except KeyError: - # Normally this should never happen. (It is a bug, if it happens.) - # But to be sure, return (0, 0) - return Point(x=0, y=0) - - # raise ValueError( - # 'Invalid position. row=%r col=%r, vertical_scroll=%r, ' - # 'horizontal_scroll=%r, height=%r' % - # (row, col, vertical_scroll, horizontal_scroll, write_position.height)) - else: - return Point(x=x, y=y) - - # Set cursor and menu positions. - if ui_content.cursor_position: - screen_cursor_position = cursor_pos_to_screen_pos( - ui_content.cursor_position.y, ui_content.cursor_position.x - ) - - if has_focus: - new_screen.set_cursor_position(self, screen_cursor_position) - - if always_hide_cursor: - new_screen.show_cursor = False - else: - new_screen.show_cursor = ui_content.show_cursor - - self._highlight_digraph(new_screen) - - if highlight_lines: - self._highlight_cursorlines( - new_screen, - screen_cursor_position, - xpos, - ypos, - width, - write_position.height, - ) - - # Draw input characters from the input processor queue. - if has_focus and ui_content.cursor_position: - self._show_key_processor_key_buffer(new_screen) - - # Set menu position. - if ui_content.menu_position: - new_screen.set_menu_position( - self, - cursor_pos_to_screen_pos( - ui_content.menu_position.y, ui_content.menu_position.x - ), - ) - - # Update output screen height. - new_screen.height = max(new_screen.height, ypos + write_position.height) - - return visible_line_to_row_col, rowcol_to_yx - - def _fill_bg( - self, screen: Screen, write_position: WritePosition, erase_bg: bool - ) -> None: - """ - Erase/fill the background. - (Useful for floats and when a `char` has been given.) - """ - char: Optional[str] - if callable(self.char): - char = self.char() - else: - char = self.char - - if erase_bg or char: - wp = write_position - char_obj = _CHAR_CACHE[char or " ", ""] - - for y in range(wp.ypos, wp.ypos + wp.height): - row = screen.data_buffer[y] - for x in range(wp.xpos, wp.xpos + wp.width): - row[x] = char_obj - - def _apply_style( - self, new_screen: Screen, write_position: WritePosition, parent_style: str - ) -> None: - - # Apply `self.style`. - style = parent_style + " " + to_str(self.style) - - new_screen.fill_area(write_position, style=style, after=False) - - # Apply the 'last-line' class to the last line of each Window. This can - # be used to apply an 'underline' to the user control. - wp = WritePosition( - write_position.xpos, - write_position.ypos + write_position.height - 1, - write_position.width, - 1, - ) - new_screen.fill_area(wp, "class:last-line", after=True) - - def _highlight_digraph(self, new_screen: Screen) -> None: - """ - When we are in Vi digraph mode, put a question mark underneath the - cursor. - """ - digraph_char = self._get_digraph_char() - if digraph_char: - cpos = new_screen.get_cursor_position(self) - new_screen.data_buffer[cpos.y][cpos.x] = _CHAR_CACHE[ - digraph_char, "class:digraph" - ] - - def _show_key_processor_key_buffer(self, new_screen: Screen) -> None: - """ - When the user is typing a key binding that consists of several keys, - display the last pressed key if the user is in insert mode and the key - is meaningful to be displayed. - E.g. Some people want to bind 'jj' to escape in Vi insert mode. But the - first 'j' needs to be displayed in order to get some feedback. - """ - app = get_app() - key_buffer = app.key_processor.key_buffer - - if key_buffer and _in_insert_mode() and not app.is_done: - # The textual data for the given key. (Can be a VT100 escape - # sequence.) - data = key_buffer[-1].data - - # Display only if this is a 1 cell width character. - if get_cwidth(data) == 1: - cpos = new_screen.get_cursor_position(self) - new_screen.data_buffer[cpos.y][cpos.x] = _CHAR_CACHE[ - data, "class:partial-key-binding" - ] - - def _highlight_cursorlines( - self, new_screen: Screen, cpos: Point, x: int, y: int, width: int, height: int - ) -> None: - """ - Highlight cursor row/column. - """ - cursor_line_style = " class:cursor-line " - cursor_column_style = " class:cursor-column " - - data_buffer = new_screen.data_buffer - - # Highlight cursor line. - if self.cursorline(): - row = data_buffer[cpos.y] - for x in range(x, x + width): - original_char = row[x] - row[x] = _CHAR_CACHE[ - original_char.char, original_char.style + cursor_line_style - ] - - # Highlight cursor column. - if self.cursorcolumn(): - for y2 in range(y, y + height): - row = data_buffer[y2] - original_char = row[cpos.x] - row[cpos.x] = _CHAR_CACHE[ - original_char.char, original_char.style + cursor_column_style - ] - - # Highlight color columns - colorcolumns = self.colorcolumns - if callable(colorcolumns): - colorcolumns = colorcolumns() - - for cc in colorcolumns: - assert isinstance(cc, ColorColumn) - column = cc.position - - if column < x + width: # Only draw when visible. - color_column_style = " " + cc.style - - for y2 in range(y, y + height): - row = data_buffer[y2] - original_char = row[column + x] - row[column + x] = _CHAR_CACHE[ - original_char.char, original_char.style + color_column_style - ] - - def _copy_margin( - self, - margin_content: UIContent, - new_screen: Screen, - write_position: WritePosition, - move_x: int, - width: int, - ) -> None: - """ - Copy characters from the margin screen to the real screen. - """ - xpos = write_position.xpos + move_x - ypos = write_position.ypos - - margin_write_position = WritePosition(xpos, ypos, width, write_position.height) - self._copy_body(margin_content, new_screen, margin_write_position, 0, width) - - def _scroll(self, ui_content: UIContent, width: int, height: int) -> None: - """ - Scroll body. Ensure that the cursor is visible. - """ - if self.wrap_lines(): - func = self._scroll_when_linewrapping - else: - func = self._scroll_without_linewrapping - - func(ui_content, width, height) - - def _scroll_when_linewrapping( - self, ui_content: UIContent, width: int, height: int - ) -> None: - """ - Scroll to make sure the cursor position is visible and that we maintain - the requested scroll offset. - - Set `self.horizontal_scroll/vertical_scroll`. - """ - scroll_offsets_bottom = self.scroll_offsets.bottom - scroll_offsets_top = self.scroll_offsets.top - - # We don't have horizontal scrolling. - self.horizontal_scroll = 0 - - def get_line_height(lineno: int) -> int: - return ui_content.get_height_for_line(lineno, width, self.get_line_prefix) - - # When there is no space, reset `vertical_scroll_2` to zero and abort. - # This can happen if the margin is bigger than the window width. - # Otherwise the text height will become "infinite" (a big number) and - # the copy_line will spend a huge amount of iterations trying to render - # nothing. - if width <= 0: - self.vertical_scroll = ui_content.cursor_position.y - self.vertical_scroll_2 = 0 - return - - # If the current line consumes more than the whole window height, - # then we have to scroll vertically inside this line. (We don't take - # the scroll offsets into account for this.) - # Also, ignore the scroll offsets in this case. Just set the vertical - # scroll to this line. - line_height = get_line_height(ui_content.cursor_position.y) - if line_height > height - scroll_offsets_top: - # Calculate the height of the text before the cursor (including - # line prefixes). - text_before_height = ui_content.get_height_for_line( - ui_content.cursor_position.y, - width, - self.get_line_prefix, - slice_stop=ui_content.cursor_position.x, - ) - - # Adjust scroll offset. - self.vertical_scroll = ui_content.cursor_position.y - self.vertical_scroll_2 = min( - text_before_height - 1, # Keep the cursor visible. - line_height - - height, # Avoid blank lines at the bottom when scolling up again. - self.vertical_scroll_2, - ) - self.vertical_scroll_2 = max( - 0, text_before_height - height, self.vertical_scroll_2 - ) - return - else: - self.vertical_scroll_2 = 0 - - # Current line doesn't consume the whole height. Take scroll offsets into account. - def get_min_vertical_scroll() -> int: - # Make sure that the cursor line is not below the bottom. - # (Calculate how many lines can be shown between the cursor and the .) - used_height = 0 - prev_lineno = ui_content.cursor_position.y - - for lineno in range(ui_content.cursor_position.y, -1, -1): - used_height += get_line_height(lineno) - - if used_height > height - scroll_offsets_bottom: - return prev_lineno - else: - prev_lineno = lineno - return 0 - - def get_max_vertical_scroll() -> int: - # Make sure that the cursor line is not above the top. - prev_lineno = ui_content.cursor_position.y - used_height = 0 - - for lineno in range(ui_content.cursor_position.y - 1, -1, -1): - used_height += get_line_height(lineno) - - if used_height > scroll_offsets_top: - return prev_lineno - else: - prev_lineno = lineno - return prev_lineno - - def get_topmost_visible() -> int: - """ - Calculate the upper most line that can be visible, while the bottom - is still visible. We should not allow scroll more than this if - `allow_scroll_beyond_bottom` is false. - """ - prev_lineno = ui_content.line_count - 1 - used_height = 0 - for lineno in range(ui_content.line_count - 1, -1, -1): - used_height += get_line_height(lineno) - if used_height > height: - return prev_lineno - else: - prev_lineno = lineno - return prev_lineno - - # Scroll vertically. (Make sure that the whole line which contains the - # cursor is visible. - topmost_visible = get_topmost_visible() - - # Note: the `min(topmost_visible, ...)` is to make sure that we - # don't require scrolling up because of the bottom scroll offset, - # when we are at the end of the document. - self.vertical_scroll = max( - self.vertical_scroll, min(topmost_visible, get_min_vertical_scroll()) - ) - self.vertical_scroll = min(self.vertical_scroll, get_max_vertical_scroll()) - - # Disallow scrolling beyond bottom? - if not self.allow_scroll_beyond_bottom(): - self.vertical_scroll = min(self.vertical_scroll, topmost_visible) - - def _scroll_without_linewrapping( - self, ui_content: UIContent, width: int, height: int - ) -> None: - """ - Scroll to make sure the cursor position is visible and that we maintain - the requested scroll offset. - - Set `self.horizontal_scroll/vertical_scroll`. - """ - cursor_position = ui_content.cursor_position or Point(x=0, y=0) - - # Without line wrapping, we will never have to scroll vertically inside - # a single line. - self.vertical_scroll_2 = 0 - - if ui_content.line_count == 0: - self.vertical_scroll = 0 - self.horizontal_scroll = 0 - return - else: - current_line_text = fragment_list_to_text( - ui_content.get_line(cursor_position.y) - ) - - def do_scroll( - current_scroll: int, - scroll_offset_start: int, - scroll_offset_end: int, - cursor_pos: int, - window_size: int, - content_size: int, - ) -> int: - "Scrolling algorithm. Used for both horizontal and vertical scrolling." - # Calculate the scroll offset to apply. - # This can obviously never be more than have the screen size. Also, when the - # cursor appears at the top or bottom, we don't apply the offset. - scroll_offset_start = int( - min(scroll_offset_start, window_size / 2, cursor_pos) - ) - scroll_offset_end = int( - min(scroll_offset_end, window_size / 2, content_size - 1 - cursor_pos) - ) - - # Prevent negative scroll offsets. - if current_scroll < 0: - current_scroll = 0 - - # Scroll back if we scrolled to much and there's still space to show more of the document. - if ( - not self.allow_scroll_beyond_bottom() - and current_scroll > content_size - window_size - ): - current_scroll = max(0, content_size - window_size) - - # Scroll up if cursor is before visible part. - if current_scroll > cursor_pos - scroll_offset_start: - current_scroll = max(0, cursor_pos - scroll_offset_start) - - # Scroll down if cursor is after visible part. - if current_scroll < (cursor_pos + 1) - window_size + scroll_offset_end: - current_scroll = (cursor_pos + 1) - window_size + scroll_offset_end - - return current_scroll - - # When a preferred scroll is given, take that first into account. - if self.get_vertical_scroll: - self.vertical_scroll = self.get_vertical_scroll(self) - assert isinstance(self.vertical_scroll, int) - if self.get_horizontal_scroll: - self.horizontal_scroll = self.get_horizontal_scroll(self) - assert isinstance(self.horizontal_scroll, int) - - # Update horizontal/vertical scroll to make sure that the cursor - # remains visible. - offsets = self.scroll_offsets - - self.vertical_scroll = do_scroll( - current_scroll=self.vertical_scroll, - scroll_offset_start=offsets.top, - scroll_offset_end=offsets.bottom, - cursor_pos=ui_content.cursor_position.y, - window_size=height, - content_size=ui_content.line_count, - ) - - if self.get_line_prefix: - current_line_prefix_width = fragment_list_width( - to_formatted_text(self.get_line_prefix(ui_content.cursor_position.y, 0)) - ) - else: - current_line_prefix_width = 0 - - self.horizontal_scroll = do_scroll( - current_scroll=self.horizontal_scroll, - scroll_offset_start=offsets.left, - scroll_offset_end=offsets.right, - cursor_pos=get_cwidth(current_line_text[: ui_content.cursor_position.x]), - window_size=width - current_line_prefix_width, - # We can only analyse the current line. Calculating the width off - # all the lines is too expensive. - content_size=max( - get_cwidth(current_line_text), self.horizontal_scroll + width - ), - ) - - def _mouse_handler(self, mouse_event: MouseEvent) -> "NotImplementedOrNone": - """ - Mouse handler. Called when the UI control doesn't handle this - particular event. - - Return `NotImplemented` if nothing was done as a consequence of this - key binding (no UI invalidate required in that case). - """ - if mouse_event.event_type == MouseEventType.SCROLL_DOWN: - self._scroll_down() - return None - elif mouse_event.event_type == MouseEventType.SCROLL_UP: - self._scroll_up() - return None - - return NotImplemented - - def _scroll_down(self) -> None: - "Scroll window down." - info = self.render_info - - if info is None: - return - - if self.vertical_scroll < info.content_height - info.window_height: - if info.cursor_position.y <= info.configured_scroll_offsets.top: - self.content.move_cursor_down() - - self.vertical_scroll += 1 - - def _scroll_up(self) -> None: - "Scroll window up." - info = self.render_info - - if info is None: - return - - if info.vertical_scroll > 0: - # TODO: not entirely correct yet in case of line wrapping and long lines. - if ( - info.cursor_position.y - >= info.window_height - 1 - info.configured_scroll_offsets.bottom - ): - self.content.move_cursor_up() - - self.vertical_scroll -= 1 - - def get_key_bindings(self) -> Optional[KeyBindingsBase]: - return self.content.get_key_bindings() - - def get_children(self) -> List[Container]: - return [] - - -class ConditionalContainer(Container): - """ - Wrapper around any other container that can change the visibility. The - received `filter` determines whether the given container should be - displayed or not. - - :param content: :class:`.Container` instance. - :param filter: :class:`.Filter` instance. - """ - - def __init__(self, content: AnyContainer, filter: FilterOrBool) -> None: - self.content = to_container(content) - self.filter = to_filter(filter) - - def __repr__(self) -> str: - return "ConditionalContainer(%r, filter=%r)" % (self.content, self.filter) - - def reset(self) -> None: - self.content.reset() - - def preferred_width(self, max_available_width: int) -> Dimension: - if self.filter(): - return self.content.preferred_width(max_available_width) - else: - return Dimension.zero() - - def preferred_height(self, width: int, max_available_height: int) -> Dimension: - if self.filter(): - return self.content.preferred_height(width, max_available_height) - else: - return Dimension.zero() - - def write_to_screen( - self, - screen: Screen, - mouse_handlers: MouseHandlers, - write_position: WritePosition, - parent_style: str, - erase_bg: bool, - z_index: Optional[int], - ) -> None: - if self.filter(): - return self.content.write_to_screen( - screen, mouse_handlers, write_position, parent_style, erase_bg, z_index - ) - - def get_children(self) -> List[Container]: - return [self.content] - - -class DynamicContainer(Container): - """ - Container class that dynamically returns any Container. - - :param get_container: Callable that returns a :class:`.Container` instance - or any widget with a ``__pt_container__`` method. - """ - - def __init__(self, get_container: Callable[[], AnyContainer]) -> None: - self.get_container = get_container - - def _get_container(self) -> Container: - """ - Return the current container object. - - We call `to_container`, because `get_container` can also return a - widget with a ``__pt_container__`` method. - """ - obj = self.get_container() - return to_container(obj) - - def reset(self) -> None: - self._get_container().reset() - - def preferred_width(self, max_available_width: int) -> Dimension: - return self._get_container().preferred_width(max_available_width) - - def preferred_height(self, width: int, max_available_height: int) -> Dimension: - return self._get_container().preferred_height(width, max_available_height) - - def write_to_screen( - self, - screen: Screen, - mouse_handlers: MouseHandlers, - write_position: WritePosition, - parent_style: str, - erase_bg: bool, - z_index: Optional[int], - ) -> None: - self._get_container().write_to_screen( - screen, mouse_handlers, write_position, parent_style, erase_bg, z_index - ) - - def is_modal(self) -> bool: - return False - - def get_key_bindings(self) -> Optional[KeyBindingsBase]: - # Key bindings will be collected when `layout.walk()` finds the child - # container. - return None - - def get_children(self) -> List[Container]: - # Here we have to return the current active container itself, not its - # children. Otherwise, we run into issues where `layout.walk()` will - # never see an object of type `Window` if this contains a window. We - # can't/shouldn't proxy the "isinstance" check. - return [self._get_container()] - - -def to_container(container: AnyContainer) -> Container: - """ - Make sure that the given object is a :class:`.Container`. - """ - if isinstance(container, Container): - return container - elif hasattr(container, "__pt_container__"): - return to_container(container.__pt_container__()) - else: - raise ValueError("Not a container object: %r" % (container,)) - - -def to_window(container: AnyContainer) -> Window: - """ - Make sure that the given argument is a :class:`.Window`. - """ - if isinstance(container, Window): - return container - elif hasattr(container, "__pt_container__"): - return to_window(cast("MagicContainer", container).__pt_container__()) - else: - raise ValueError("Not a Window object: %r." % (container,)) - - -def is_container(value: object) -> "TypeGuard[AnyContainer]": - """ - Checks whether the given value is a container object - (for use in assert statements). - """ - if isinstance(value, Container): - return True - if hasattr(value, "__pt_container__"): - return is_container(cast("MagicContainer", value).__pt_container__()) - return False + new_buffer_row[x + xpos] = char + + # When we print a multi width character, make sure + # to erase the neighbours positions in the screen. + # (The empty string if different from everything, + # so next redraw this cell will repaint anyway.) + if char_width > 1: + for i in range(1, char_width): + new_buffer_row[x + xpos + i] = empty_char + + # If this is a zero width characters, then it's + # probably part of a decomposed unicode character. + # See: https://en.wikipedia.org/wiki/Unicode_equivalence + # Merge it in the previous cell. + elif char_width == 0: + # Handle all character widths. If the previous + # character is a multiwidth character, then + # merge it two positions back. + for pw in [2, 1]: # Previous character width. + if ( + x - pw >= 0 + and new_buffer_row[x + xpos - pw].width == pw + ): + prev_char = new_buffer_row[x + xpos - pw] + char2 = _CHAR_CACHE[ + prev_char.char + c, prev_char.style + ] + new_buffer_row[x + xpos - pw] = char2 + + # Keep track of write position for each character. + current_rowcol_to_yx[lineno, col + skipped] = ( + y + ypos, + x + xpos, + ) + + col += 1 + x += char_width + return x, y + + # Copy content. + def copy() -> int: + y = -vertical_scroll_2 + lineno = vertical_scroll + + while y < write_position.height and lineno < line_count: + # Take the next line and copy it in the real screen. + line = ui_content.get_line(lineno) + + visible_line_to_row_col[y] = (lineno, horizontal_scroll) + + # Copy margin and actual line. + x = 0 + x, y = copy_line(line, lineno, x, y, is_input=True) + + lineno += 1 + y += 1 + return y + + copy() + + def cursor_pos_to_screen_pos(row: int, col: int) -> Point: + "Translate row/col from UIContent to real Screen coordinates." + try: + y, x = rowcol_to_yx[row, col] + except KeyError: + # Normally this should never happen. (It is a bug, if it happens.) + # But to be sure, return (0, 0) + return Point(x=0, y=0) + + # raise ValueError( + # 'Invalid position. row=%r col=%r, vertical_scroll=%r, ' + # 'horizontal_scroll=%r, height=%r' % + # (row, col, vertical_scroll, horizontal_scroll, write_position.height)) + else: + return Point(x=x, y=y) + + # Set cursor and menu positions. + if ui_content.cursor_position: + screen_cursor_position = cursor_pos_to_screen_pos( + ui_content.cursor_position.y, ui_content.cursor_position.x + ) + + if has_focus: + new_screen.set_cursor_position(self, screen_cursor_position) + + if always_hide_cursor: + new_screen.show_cursor = False + else: + new_screen.show_cursor = ui_content.show_cursor + + self._highlight_digraph(new_screen) + + if highlight_lines: + self._highlight_cursorlines( + new_screen, + screen_cursor_position, + xpos, + ypos, + width, + write_position.height, + ) + + # Draw input characters from the input processor queue. + if has_focus and ui_content.cursor_position: + self._show_key_processor_key_buffer(new_screen) + + # Set menu position. + if ui_content.menu_position: + new_screen.set_menu_position( + self, + cursor_pos_to_screen_pos( + ui_content.menu_position.y, ui_content.menu_position.x + ), + ) + + # Update output screen height. + new_screen.height = max(new_screen.height, ypos + write_position.height) + + return visible_line_to_row_col, rowcol_to_yx + + def _fill_bg( + self, screen: Screen, write_position: WritePosition, erase_bg: bool + ) -> None: + """ + Erase/fill the background. + (Useful for floats and when a `char` has been given.) + """ + char: Optional[str] + if callable(self.char): + char = self.char() + else: + char = self.char + + if erase_bg or char: + wp = write_position + char_obj = _CHAR_CACHE[char or " ", ""] + + for y in range(wp.ypos, wp.ypos + wp.height): + row = screen.data_buffer[y] + for x in range(wp.xpos, wp.xpos + wp.width): + row[x] = char_obj + + def _apply_style( + self, new_screen: Screen, write_position: WritePosition, parent_style: str + ) -> None: + + # Apply `self.style`. + style = parent_style + " " + to_str(self.style) + + new_screen.fill_area(write_position, style=style, after=False) + + # Apply the 'last-line' class to the last line of each Window. This can + # be used to apply an 'underline' to the user control. + wp = WritePosition( + write_position.xpos, + write_position.ypos + write_position.height - 1, + write_position.width, + 1, + ) + new_screen.fill_area(wp, "class:last-line", after=True) + + def _highlight_digraph(self, new_screen: Screen) -> None: + """ + When we are in Vi digraph mode, put a question mark underneath the + cursor. + """ + digraph_char = self._get_digraph_char() + if digraph_char: + cpos = new_screen.get_cursor_position(self) + new_screen.data_buffer[cpos.y][cpos.x] = _CHAR_CACHE[ + digraph_char, "class:digraph" + ] + + def _show_key_processor_key_buffer(self, new_screen: Screen) -> None: + """ + When the user is typing a key binding that consists of several keys, + display the last pressed key if the user is in insert mode and the key + is meaningful to be displayed. + E.g. Some people want to bind 'jj' to escape in Vi insert mode. But the + first 'j' needs to be displayed in order to get some feedback. + """ + app = get_app() + key_buffer = app.key_processor.key_buffer + + if key_buffer and _in_insert_mode() and not app.is_done: + # The textual data for the given key. (Can be a VT100 escape + # sequence.) + data = key_buffer[-1].data + + # Display only if this is a 1 cell width character. + if get_cwidth(data) == 1: + cpos = new_screen.get_cursor_position(self) + new_screen.data_buffer[cpos.y][cpos.x] = _CHAR_CACHE[ + data, "class:partial-key-binding" + ] + + def _highlight_cursorlines( + self, new_screen: Screen, cpos: Point, x: int, y: int, width: int, height: int + ) -> None: + """ + Highlight cursor row/column. + """ + cursor_line_style = " class:cursor-line " + cursor_column_style = " class:cursor-column " + + data_buffer = new_screen.data_buffer + + # Highlight cursor line. + if self.cursorline(): + row = data_buffer[cpos.y] + for x in range(x, x + width): + original_char = row[x] + row[x] = _CHAR_CACHE[ + original_char.char, original_char.style + cursor_line_style + ] + + # Highlight cursor column. + if self.cursorcolumn(): + for y2 in range(y, y + height): + row = data_buffer[y2] + original_char = row[cpos.x] + row[cpos.x] = _CHAR_CACHE[ + original_char.char, original_char.style + cursor_column_style + ] + + # Highlight color columns + colorcolumns = self.colorcolumns + if callable(colorcolumns): + colorcolumns = colorcolumns() + + for cc in colorcolumns: + assert isinstance(cc, ColorColumn) + column = cc.position + + if column < x + width: # Only draw when visible. + color_column_style = " " + cc.style + + for y2 in range(y, y + height): + row = data_buffer[y2] + original_char = row[column + x] + row[column + x] = _CHAR_CACHE[ + original_char.char, original_char.style + color_column_style + ] + + def _copy_margin( + self, + margin_content: UIContent, + new_screen: Screen, + write_position: WritePosition, + move_x: int, + width: int, + ) -> None: + """ + Copy characters from the margin screen to the real screen. + """ + xpos = write_position.xpos + move_x + ypos = write_position.ypos + + margin_write_position = WritePosition(xpos, ypos, width, write_position.height) + self._copy_body(margin_content, new_screen, margin_write_position, 0, width) + + def _scroll(self, ui_content: UIContent, width: int, height: int) -> None: + """ + Scroll body. Ensure that the cursor is visible. + """ + if self.wrap_lines(): + func = self._scroll_when_linewrapping + else: + func = self._scroll_without_linewrapping + + func(ui_content, width, height) + + def _scroll_when_linewrapping( + self, ui_content: UIContent, width: int, height: int + ) -> None: + """ + Scroll to make sure the cursor position is visible and that we maintain + the requested scroll offset. + + Set `self.horizontal_scroll/vertical_scroll`. + """ + scroll_offsets_bottom = self.scroll_offsets.bottom + scroll_offsets_top = self.scroll_offsets.top + + # We don't have horizontal scrolling. + self.horizontal_scroll = 0 + + def get_line_height(lineno: int) -> int: + return ui_content.get_height_for_line(lineno, width, self.get_line_prefix) + + # When there is no space, reset `vertical_scroll_2` to zero and abort. + # This can happen if the margin is bigger than the window width. + # Otherwise the text height will become "infinite" (a big number) and + # the copy_line will spend a huge amount of iterations trying to render + # nothing. + if width <= 0: + self.vertical_scroll = ui_content.cursor_position.y + self.vertical_scroll_2 = 0 + return + + # If the current line consumes more than the whole window height, + # then we have to scroll vertically inside this line. (We don't take + # the scroll offsets into account for this.) + # Also, ignore the scroll offsets in this case. Just set the vertical + # scroll to this line. + line_height = get_line_height(ui_content.cursor_position.y) + if line_height > height - scroll_offsets_top: + # Calculate the height of the text before the cursor (including + # line prefixes). + text_before_height = ui_content.get_height_for_line( + ui_content.cursor_position.y, + width, + self.get_line_prefix, + slice_stop=ui_content.cursor_position.x, + ) + + # Adjust scroll offset. + self.vertical_scroll = ui_content.cursor_position.y + self.vertical_scroll_2 = min( + text_before_height - 1, # Keep the cursor visible. + line_height + - height, # Avoid blank lines at the bottom when scolling up again. + self.vertical_scroll_2, + ) + self.vertical_scroll_2 = max( + 0, text_before_height - height, self.vertical_scroll_2 + ) + return + else: + self.vertical_scroll_2 = 0 + + # Current line doesn't consume the whole height. Take scroll offsets into account. + def get_min_vertical_scroll() -> int: + # Make sure that the cursor line is not below the bottom. + # (Calculate how many lines can be shown between the cursor and the .) + used_height = 0 + prev_lineno = ui_content.cursor_position.y + + for lineno in range(ui_content.cursor_position.y, -1, -1): + used_height += get_line_height(lineno) + + if used_height > height - scroll_offsets_bottom: + return prev_lineno + else: + prev_lineno = lineno + return 0 + + def get_max_vertical_scroll() -> int: + # Make sure that the cursor line is not above the top. + prev_lineno = ui_content.cursor_position.y + used_height = 0 + + for lineno in range(ui_content.cursor_position.y - 1, -1, -1): + used_height += get_line_height(lineno) + + if used_height > scroll_offsets_top: + return prev_lineno + else: + prev_lineno = lineno + return prev_lineno + + def get_topmost_visible() -> int: + """ + Calculate the upper most line that can be visible, while the bottom + is still visible. We should not allow scroll more than this if + `allow_scroll_beyond_bottom` is false. + """ + prev_lineno = ui_content.line_count - 1 + used_height = 0 + for lineno in range(ui_content.line_count - 1, -1, -1): + used_height += get_line_height(lineno) + if used_height > height: + return prev_lineno + else: + prev_lineno = lineno + return prev_lineno + + # Scroll vertically. (Make sure that the whole line which contains the + # cursor is visible. + topmost_visible = get_topmost_visible() + + # Note: the `min(topmost_visible, ...)` is to make sure that we + # don't require scrolling up because of the bottom scroll offset, + # when we are at the end of the document. + self.vertical_scroll = max( + self.vertical_scroll, min(topmost_visible, get_min_vertical_scroll()) + ) + self.vertical_scroll = min(self.vertical_scroll, get_max_vertical_scroll()) + + # Disallow scrolling beyond bottom? + if not self.allow_scroll_beyond_bottom(): + self.vertical_scroll = min(self.vertical_scroll, topmost_visible) + + def _scroll_without_linewrapping( + self, ui_content: UIContent, width: int, height: int + ) -> None: + """ + Scroll to make sure the cursor position is visible and that we maintain + the requested scroll offset. + + Set `self.horizontal_scroll/vertical_scroll`. + """ + cursor_position = ui_content.cursor_position or Point(x=0, y=0) + + # Without line wrapping, we will never have to scroll vertically inside + # a single line. + self.vertical_scroll_2 = 0 + + if ui_content.line_count == 0: + self.vertical_scroll = 0 + self.horizontal_scroll = 0 + return + else: + current_line_text = fragment_list_to_text( + ui_content.get_line(cursor_position.y) + ) + + def do_scroll( + current_scroll: int, + scroll_offset_start: int, + scroll_offset_end: int, + cursor_pos: int, + window_size: int, + content_size: int, + ) -> int: + "Scrolling algorithm. Used for both horizontal and vertical scrolling." + # Calculate the scroll offset to apply. + # This can obviously never be more than have the screen size. Also, when the + # cursor appears at the top or bottom, we don't apply the offset. + scroll_offset_start = int( + min(scroll_offset_start, window_size / 2, cursor_pos) + ) + scroll_offset_end = int( + min(scroll_offset_end, window_size / 2, content_size - 1 - cursor_pos) + ) + + # Prevent negative scroll offsets. + if current_scroll < 0: + current_scroll = 0 + + # Scroll back if we scrolled to much and there's still space to show more of the document. + if ( + not self.allow_scroll_beyond_bottom() + and current_scroll > content_size - window_size + ): + current_scroll = max(0, content_size - window_size) + + # Scroll up if cursor is before visible part. + if current_scroll > cursor_pos - scroll_offset_start: + current_scroll = max(0, cursor_pos - scroll_offset_start) + + # Scroll down if cursor is after visible part. + if current_scroll < (cursor_pos + 1) - window_size + scroll_offset_end: + current_scroll = (cursor_pos + 1) - window_size + scroll_offset_end + + return current_scroll + + # When a preferred scroll is given, take that first into account. + if self.get_vertical_scroll: + self.vertical_scroll = self.get_vertical_scroll(self) + assert isinstance(self.vertical_scroll, int) + if self.get_horizontal_scroll: + self.horizontal_scroll = self.get_horizontal_scroll(self) + assert isinstance(self.horizontal_scroll, int) + + # Update horizontal/vertical scroll to make sure that the cursor + # remains visible. + offsets = self.scroll_offsets + + self.vertical_scroll = do_scroll( + current_scroll=self.vertical_scroll, + scroll_offset_start=offsets.top, + scroll_offset_end=offsets.bottom, + cursor_pos=ui_content.cursor_position.y, + window_size=height, + content_size=ui_content.line_count, + ) + + if self.get_line_prefix: + current_line_prefix_width = fragment_list_width( + to_formatted_text(self.get_line_prefix(ui_content.cursor_position.y, 0)) + ) + else: + current_line_prefix_width = 0 + + self.horizontal_scroll = do_scroll( + current_scroll=self.horizontal_scroll, + scroll_offset_start=offsets.left, + scroll_offset_end=offsets.right, + cursor_pos=get_cwidth(current_line_text[: ui_content.cursor_position.x]), + window_size=width - current_line_prefix_width, + # We can only analyse the current line. Calculating the width off + # all the lines is too expensive. + content_size=max( + get_cwidth(current_line_text), self.horizontal_scroll + width + ), + ) + + def _mouse_handler(self, mouse_event: MouseEvent) -> "NotImplementedOrNone": + """ + Mouse handler. Called when the UI control doesn't handle this + particular event. + + Return `NotImplemented` if nothing was done as a consequence of this + key binding (no UI invalidate required in that case). + """ + if mouse_event.event_type == MouseEventType.SCROLL_DOWN: + self._scroll_down() + return None + elif mouse_event.event_type == MouseEventType.SCROLL_UP: + self._scroll_up() + return None + + return NotImplemented + + def _scroll_down(self) -> None: + "Scroll window down." + info = self.render_info + + if info is None: + return + + if self.vertical_scroll < info.content_height - info.window_height: + if info.cursor_position.y <= info.configured_scroll_offsets.top: + self.content.move_cursor_down() + + self.vertical_scroll += 1 + + def _scroll_up(self) -> None: + "Scroll window up." + info = self.render_info + + if info is None: + return + + if info.vertical_scroll > 0: + # TODO: not entirely correct yet in case of line wrapping and long lines. + if ( + info.cursor_position.y + >= info.window_height - 1 - info.configured_scroll_offsets.bottom + ): + self.content.move_cursor_up() + + self.vertical_scroll -= 1 + + def get_key_bindings(self) -> Optional[KeyBindingsBase]: + return self.content.get_key_bindings() + + def get_children(self) -> List[Container]: + return [] + + +class ConditionalContainer(Container): + """ + Wrapper around any other container that can change the visibility. The + received `filter` determines whether the given container should be + displayed or not. + + :param content: :class:`.Container` instance. + :param filter: :class:`.Filter` instance. + """ + + def __init__(self, content: AnyContainer, filter: FilterOrBool) -> None: + self.content = to_container(content) + self.filter = to_filter(filter) + + def __repr__(self) -> str: + return "ConditionalContainer(%r, filter=%r)" % (self.content, self.filter) + + def reset(self) -> None: + self.content.reset() + + def preferred_width(self, max_available_width: int) -> Dimension: + if self.filter(): + return self.content.preferred_width(max_available_width) + else: + return Dimension.zero() + + def preferred_height(self, width: int, max_available_height: int) -> Dimension: + if self.filter(): + return self.content.preferred_height(width, max_available_height) + else: + return Dimension.zero() + + def write_to_screen( + self, + screen: Screen, + mouse_handlers: MouseHandlers, + write_position: WritePosition, + parent_style: str, + erase_bg: bool, + z_index: Optional[int], + ) -> None: + if self.filter(): + return self.content.write_to_screen( + screen, mouse_handlers, write_position, parent_style, erase_bg, z_index + ) + + def get_children(self) -> List[Container]: + return [self.content] + + +class DynamicContainer(Container): + """ + Container class that dynamically returns any Container. + + :param get_container: Callable that returns a :class:`.Container` instance + or any widget with a ``__pt_container__`` method. + """ + + def __init__(self, get_container: Callable[[], AnyContainer]) -> None: + self.get_container = get_container + + def _get_container(self) -> Container: + """ + Return the current container object. + + We call `to_container`, because `get_container` can also return a + widget with a ``__pt_container__`` method. + """ + obj = self.get_container() + return to_container(obj) + + def reset(self) -> None: + self._get_container().reset() + + def preferred_width(self, max_available_width: int) -> Dimension: + return self._get_container().preferred_width(max_available_width) + + def preferred_height(self, width: int, max_available_height: int) -> Dimension: + return self._get_container().preferred_height(width, max_available_height) + + def write_to_screen( + self, + screen: Screen, + mouse_handlers: MouseHandlers, + write_position: WritePosition, + parent_style: str, + erase_bg: bool, + z_index: Optional[int], + ) -> None: + self._get_container().write_to_screen( + screen, mouse_handlers, write_position, parent_style, erase_bg, z_index + ) + + def is_modal(self) -> bool: + return False + + def get_key_bindings(self) -> Optional[KeyBindingsBase]: + # Key bindings will be collected when `layout.walk()` finds the child + # container. + return None + + def get_children(self) -> List[Container]: + # Here we have to return the current active container itself, not its + # children. Otherwise, we run into issues where `layout.walk()` will + # never see an object of type `Window` if this contains a window. We + # can't/shouldn't proxy the "isinstance" check. + return [self._get_container()] + + +def to_container(container: AnyContainer) -> Container: + """ + Make sure that the given object is a :class:`.Container`. + """ + if isinstance(container, Container): + return container + elif hasattr(container, "__pt_container__"): + return to_container(container.__pt_container__()) + else: + raise ValueError("Not a container object: %r" % (container,)) + + +def to_window(container: AnyContainer) -> Window: + """ + Make sure that the given argument is a :class:`.Window`. + """ + if isinstance(container, Window): + return container + elif hasattr(container, "__pt_container__"): + return to_window(cast("MagicContainer", container).__pt_container__()) + else: + raise ValueError("Not a Window object: %r." % (container,)) + + +def is_container(value: object) -> "TypeGuard[AnyContainer]": + """ + Checks whether the given value is a container object + (for use in assert statements). + """ + if isinstance(value, Container): + return True + if hasattr(value, "__pt_container__"): + return is_container(cast("MagicContainer", value).__pt_container__()) + return False diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/controls.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/controls.py index 4810ed5dd4..45b50e68f8 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/controls.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/controls.py @@ -1,961 +1,961 @@ -""" -User interface Controls for the layout. -""" -import time -from abc import ABCMeta, abstractmethod -from typing import ( - TYPE_CHECKING, - Callable, - Dict, - Hashable, - Iterable, - List, - NamedTuple, - Optional, - Union, -) - -from prompt_toolkit.application.current import get_app -from prompt_toolkit.buffer import Buffer -from prompt_toolkit.cache import SimpleCache -from prompt_toolkit.data_structures import Point -from prompt_toolkit.document import Document -from prompt_toolkit.filters import FilterOrBool, to_filter -from prompt_toolkit.formatted_text import ( - AnyFormattedText, - StyleAndTextTuples, - to_formatted_text, -) -from prompt_toolkit.formatted_text.utils import ( - fragment_list_to_text, - fragment_list_width, - split_lines, -) -from prompt_toolkit.lexers import Lexer, SimpleLexer -from prompt_toolkit.mouse_events import MouseButton, MouseEvent, MouseEventType -from prompt_toolkit.search import SearchState -from prompt_toolkit.selection import SelectionType -from prompt_toolkit.utils import get_cwidth - -from .processors import ( - DisplayMultipleCursors, - HighlightIncrementalSearchProcessor, - HighlightSearchProcessor, - HighlightSelectionProcessor, - Processor, - TransformationInput, - merge_processors, -) - -if TYPE_CHECKING: - from prompt_toolkit.key_binding.key_bindings import ( - KeyBindingsBase, - NotImplementedOrNone, - ) - from prompt_toolkit.utils import Event - - -__all__ = [ - "BufferControl", - "SearchBufferControl", - "DummyControl", - "FormattedTextControl", - "UIControl", - "UIContent", -] - -GetLinePrefixCallable = Callable[[int, int], AnyFormattedText] - - -class UIControl(metaclass=ABCMeta): - """ - Base class for all user interface controls. - """ - - def reset(self) -> None: - # Default reset. (Doesn't have to be implemented.) - pass - - def preferred_width(self, max_available_width: int) -> Optional[int]: - return None - - def preferred_height( - self, - width: int, - max_available_height: int, - wrap_lines: bool, - get_line_prefix: Optional[GetLinePrefixCallable], - ) -> Optional[int]: - return None - - def is_focusable(self) -> bool: - """ - Tell whether this user control is focusable. - """ - return False - - @abstractmethod - def create_content(self, width: int, height: int) -> "UIContent": - """ - Generate the content for this user control. - - Returns a :class:`.UIContent` instance. - """ - - def mouse_handler(self, mouse_event: MouseEvent) -> "NotImplementedOrNone": - """ - Handle mouse events. - - When `NotImplemented` is returned, it means that the given event is not - handled by the `UIControl` itself. The `Window` or key bindings can - decide to handle this event as scrolling or changing focus. - - :param mouse_event: `MouseEvent` instance. - """ - return NotImplemented - - def move_cursor_down(self) -> None: - """ - Request to move the cursor down. - This happens when scrolling down and the cursor is completely at the - top. - """ - - def move_cursor_up(self) -> None: - """ - Request to move the cursor up. - """ - - def get_key_bindings(self) -> Optional["KeyBindingsBase"]: - """ - The key bindings that are specific for this user control. - - Return a :class:`.KeyBindings` object if some key bindings are - specified, or `None` otherwise. - """ - - def get_invalidate_events(self) -> Iterable["Event[object]"]: - """ - Return a list of `Event` objects. This can be a generator. - (The application collects all these events, in order to bind redraw - handlers to these events.) - """ - return [] - - -class UIContent: - """ - Content generated by a user control. This content consists of a list of - lines. - - :param get_line: Callable that takes a line number and returns the current - line. This is a list of (style_str, text) tuples. - :param line_count: The number of lines. - :param cursor_position: a :class:`.Point` for the cursor position. - :param menu_position: a :class:`.Point` for the menu position. - :param show_cursor: Make the cursor visible. - """ - - def __init__( - self, - get_line: Callable[[int], StyleAndTextTuples] = (lambda i: []), - line_count: int = 0, - cursor_position: Optional[Point] = None, - menu_position: Optional[Point] = None, - show_cursor: bool = True, - ): - - self.get_line = get_line - self.line_count = line_count - self.cursor_position = cursor_position or Point(x=0, y=0) - self.menu_position = menu_position - self.show_cursor = show_cursor - - # Cache for line heights. Maps cache key -> height - self._line_heights_cache: Dict[Hashable, int] = {} - - def __getitem__(self, lineno: int) -> StyleAndTextTuples: - "Make it iterable (iterate line by line)." - if lineno < self.line_count: - return self.get_line(lineno) - else: - raise IndexError - - def get_height_for_line( - self, - lineno: int, - width: int, - get_line_prefix: Optional[GetLinePrefixCallable], - slice_stop: Optional[int] = None, - ) -> int: - """ - Return the height that a given line would need if it is rendered in a - space with the given width (using line wrapping). - - :param get_line_prefix: None or a `Window.get_line_prefix` callable - that returns the prefix to be inserted before this line. - :param slice_stop: Wrap only "line[:slice_stop]" and return that - partial result. This is needed for scrolling the window correctly - when line wrapping. - :returns: The computed height. - """ - # Instead of using `get_line_prefix` as key, we use render_counter - # instead. This is more reliable, because this function could still be - # the same, while the content would change over time. - key = get_app().render_counter, lineno, width, slice_stop - - try: - return self._line_heights_cache[key] - except KeyError: - if width == 0: +""" +User interface Controls for the layout. +""" +import time +from abc import ABCMeta, abstractmethod +from typing import ( + TYPE_CHECKING, + Callable, + Dict, + Hashable, + Iterable, + List, + NamedTuple, + Optional, + Union, +) + +from prompt_toolkit.application.current import get_app +from prompt_toolkit.buffer import Buffer +from prompt_toolkit.cache import SimpleCache +from prompt_toolkit.data_structures import Point +from prompt_toolkit.document import Document +from prompt_toolkit.filters import FilterOrBool, to_filter +from prompt_toolkit.formatted_text import ( + AnyFormattedText, + StyleAndTextTuples, + to_formatted_text, +) +from prompt_toolkit.formatted_text.utils import ( + fragment_list_to_text, + fragment_list_width, + split_lines, +) +from prompt_toolkit.lexers import Lexer, SimpleLexer +from prompt_toolkit.mouse_events import MouseButton, MouseEvent, MouseEventType +from prompt_toolkit.search import SearchState +from prompt_toolkit.selection import SelectionType +from prompt_toolkit.utils import get_cwidth + +from .processors import ( + DisplayMultipleCursors, + HighlightIncrementalSearchProcessor, + HighlightSearchProcessor, + HighlightSelectionProcessor, + Processor, + TransformationInput, + merge_processors, +) + +if TYPE_CHECKING: + from prompt_toolkit.key_binding.key_bindings import ( + KeyBindingsBase, + NotImplementedOrNone, + ) + from prompt_toolkit.utils import Event + + +__all__ = [ + "BufferControl", + "SearchBufferControl", + "DummyControl", + "FormattedTextControl", + "UIControl", + "UIContent", +] + +GetLinePrefixCallable = Callable[[int, int], AnyFormattedText] + + +class UIControl(metaclass=ABCMeta): + """ + Base class for all user interface controls. + """ + + def reset(self) -> None: + # Default reset. (Doesn't have to be implemented.) + pass + + def preferred_width(self, max_available_width: int) -> Optional[int]: + return None + + def preferred_height( + self, + width: int, + max_available_height: int, + wrap_lines: bool, + get_line_prefix: Optional[GetLinePrefixCallable], + ) -> Optional[int]: + return None + + def is_focusable(self) -> bool: + """ + Tell whether this user control is focusable. + """ + return False + + @abstractmethod + def create_content(self, width: int, height: int) -> "UIContent": + """ + Generate the content for this user control. + + Returns a :class:`.UIContent` instance. + """ + + def mouse_handler(self, mouse_event: MouseEvent) -> "NotImplementedOrNone": + """ + Handle mouse events. + + When `NotImplemented` is returned, it means that the given event is not + handled by the `UIControl` itself. The `Window` or key bindings can + decide to handle this event as scrolling or changing focus. + + :param mouse_event: `MouseEvent` instance. + """ + return NotImplemented + + def move_cursor_down(self) -> None: + """ + Request to move the cursor down. + This happens when scrolling down and the cursor is completely at the + top. + """ + + def move_cursor_up(self) -> None: + """ + Request to move the cursor up. + """ + + def get_key_bindings(self) -> Optional["KeyBindingsBase"]: + """ + The key bindings that are specific for this user control. + + Return a :class:`.KeyBindings` object if some key bindings are + specified, or `None` otherwise. + """ + + def get_invalidate_events(self) -> Iterable["Event[object]"]: + """ + Return a list of `Event` objects. This can be a generator. + (The application collects all these events, in order to bind redraw + handlers to these events.) + """ + return [] + + +class UIContent: + """ + Content generated by a user control. This content consists of a list of + lines. + + :param get_line: Callable that takes a line number and returns the current + line. This is a list of (style_str, text) tuples. + :param line_count: The number of lines. + :param cursor_position: a :class:`.Point` for the cursor position. + :param menu_position: a :class:`.Point` for the menu position. + :param show_cursor: Make the cursor visible. + """ + + def __init__( + self, + get_line: Callable[[int], StyleAndTextTuples] = (lambda i: []), + line_count: int = 0, + cursor_position: Optional[Point] = None, + menu_position: Optional[Point] = None, + show_cursor: bool = True, + ): + + self.get_line = get_line + self.line_count = line_count + self.cursor_position = cursor_position or Point(x=0, y=0) + self.menu_position = menu_position + self.show_cursor = show_cursor + + # Cache for line heights. Maps cache key -> height + self._line_heights_cache: Dict[Hashable, int] = {} + + def __getitem__(self, lineno: int) -> StyleAndTextTuples: + "Make it iterable (iterate line by line)." + if lineno < self.line_count: + return self.get_line(lineno) + else: + raise IndexError + + def get_height_for_line( + self, + lineno: int, + width: int, + get_line_prefix: Optional[GetLinePrefixCallable], + slice_stop: Optional[int] = None, + ) -> int: + """ + Return the height that a given line would need if it is rendered in a + space with the given width (using line wrapping). + + :param get_line_prefix: None or a `Window.get_line_prefix` callable + that returns the prefix to be inserted before this line. + :param slice_stop: Wrap only "line[:slice_stop]" and return that + partial result. This is needed for scrolling the window correctly + when line wrapping. + :returns: The computed height. + """ + # Instead of using `get_line_prefix` as key, we use render_counter + # instead. This is more reliable, because this function could still be + # the same, while the content would change over time. + key = get_app().render_counter, lineno, width, slice_stop + + try: + return self._line_heights_cache[key] + except KeyError: + if width == 0: height = 10**8 - else: - # Calculate line width first. - line = fragment_list_to_text(self.get_line(lineno))[:slice_stop] - text_width = get_cwidth(line) - - if get_line_prefix: - # Add prefix width. - text_width += fragment_list_width( - to_formatted_text(get_line_prefix(lineno, 0)) - ) - - # Slower path: compute path when there's a line prefix. - height = 1 - - # Keep wrapping as long as the line doesn't fit. - # Keep adding new prefixes for every wrapped line. - while text_width > width: - height += 1 - text_width -= width - - fragments2 = to_formatted_text( - get_line_prefix(lineno, height - 1) - ) - prefix_width = get_cwidth(fragment_list_to_text(fragments2)) - - if prefix_width >= width: # Prefix doesn't fit. + else: + # Calculate line width first. + line = fragment_list_to_text(self.get_line(lineno))[:slice_stop] + text_width = get_cwidth(line) + + if get_line_prefix: + # Add prefix width. + text_width += fragment_list_width( + to_formatted_text(get_line_prefix(lineno, 0)) + ) + + # Slower path: compute path when there's a line prefix. + height = 1 + + # Keep wrapping as long as the line doesn't fit. + # Keep adding new prefixes for every wrapped line. + while text_width > width: + height += 1 + text_width -= width + + fragments2 = to_formatted_text( + get_line_prefix(lineno, height - 1) + ) + prefix_width = get_cwidth(fragment_list_to_text(fragments2)) + + if prefix_width >= width: # Prefix doesn't fit. height = 10**8 - break - - text_width += prefix_width - else: - # Fast path: compute height when there's no line prefix. - try: - quotient, remainder = divmod(text_width, width) - except ZeroDivisionError: + break + + text_width += prefix_width + else: + # Fast path: compute height when there's no line prefix. + try: + quotient, remainder = divmod(text_width, width) + except ZeroDivisionError: height = 10**8 - else: - if remainder: - quotient += 1 # Like math.ceil. - height = max(1, quotient) - - # Cache and return - self._line_heights_cache[key] = height - return height - - -class FormattedTextControl(UIControl): - """ - Control that displays formatted text. This can be either plain text, an - :class:`~prompt_toolkit.formatted_text.HTML` object an - :class:`~prompt_toolkit.formatted_text.ANSI` object, a list of ``(style_str, - text)`` tuples or a callable that takes no argument and returns one of - those, depending on how you prefer to do the formatting. See - ``prompt_toolkit.layout.formatted_text`` for more information. - - (It's mostly optimized for rather small widgets, like toolbars, menus, etc...) - - When this UI control has the focus, the cursor will be shown in the upper - left corner of this control by default. There are two ways for specifying - the cursor position: - - - Pass a `get_cursor_position` function which returns a `Point` instance - with the current cursor position. - - - If the (formatted) text is passed as a list of ``(style, text)`` tuples - and there is one that looks like ``('[SetCursorPosition]', '')``, then - this will specify the cursor position. - - Mouse support: - - The list of fragments can also contain tuples of three items, looking like: - (style_str, text, handler). When mouse support is enabled and the user - clicks on this fragment, then the given handler is called. That handler - should accept two inputs: (Application, MouseEvent) and it should - either handle the event or return `NotImplemented` in case we want the - containing Window to handle this event. - - :param focusable: `bool` or :class:`.Filter`: Tell whether this control is - focusable. - - :param text: Text or formatted text to be displayed. - :param style: Style string applied to the content. (If you want to style - the whole :class:`~prompt_toolkit.layout.Window`, pass the style to the - :class:`~prompt_toolkit.layout.Window` instead.) - :param key_bindings: a :class:`.KeyBindings` object. - :param get_cursor_position: A callable that returns the cursor position as - a `Point` instance. - """ - - def __init__( - self, - text: AnyFormattedText = "", - style: str = "", - focusable: FilterOrBool = False, - key_bindings: Optional["KeyBindingsBase"] = None, - show_cursor: bool = True, - modal: bool = False, - get_cursor_position: Optional[Callable[[], Optional[Point]]] = None, - ) -> None: - - self.text = text # No type check on 'text'. This is done dynamically. - self.style = style - self.focusable = to_filter(focusable) - - # Key bindings. - self.key_bindings = key_bindings - self.show_cursor = show_cursor - self.modal = modal - self.get_cursor_position = get_cursor_position - - #: Cache for the content. - self._content_cache: SimpleCache[Hashable, UIContent] = SimpleCache(maxsize=18) - self._fragment_cache: SimpleCache[int, StyleAndTextTuples] = SimpleCache( - maxsize=1 - ) - # Only cache one fragment list. We don't need the previous item. - - # Render info for the mouse support. - self._fragments: Optional[StyleAndTextTuples] = None - - def reset(self) -> None: - self._fragments = None - - def is_focusable(self) -> bool: - return self.focusable() - - def __repr__(self) -> str: - return "%s(%r)" % (self.__class__.__name__, self.text) - - def _get_formatted_text_cached(self) -> StyleAndTextTuples: - """ - Get fragments, but only retrieve fragments once during one render run. - (This function is called several times during one rendering, because - we also need those for calculating the dimensions.) - """ - return self._fragment_cache.get( - get_app().render_counter, lambda: to_formatted_text(self.text, self.style) - ) - - def preferred_width(self, max_available_width: int) -> int: - """ - Return the preferred width for this control. - That is the width of the longest line. - """ - text = fragment_list_to_text(self._get_formatted_text_cached()) - line_lengths = [get_cwidth(l) for l in text.split("\n")] - return max(line_lengths) - - def preferred_height( - self, - width: int, - max_available_height: int, - wrap_lines: bool, - get_line_prefix: Optional[GetLinePrefixCallable], - ) -> Optional[int]: - """ - Return the preferred height for this control. - """ - content = self.create_content(width, None) - if wrap_lines: - height = 0 - for i in range(content.line_count): - height += content.get_height_for_line(i, width, get_line_prefix) - if height >= max_available_height: - return max_available_height - return height - else: - return content.line_count - - def create_content(self, width: int, height: Optional[int]) -> UIContent: - # Get fragments - fragments_with_mouse_handlers = self._get_formatted_text_cached() - fragment_lines_with_mouse_handlers = list( - split_lines(fragments_with_mouse_handlers) - ) - - # Strip mouse handlers from fragments. - fragment_lines: List[StyleAndTextTuples] = [ - [(item[0], item[1]) for item in line] - for line in fragment_lines_with_mouse_handlers - ] - - # Keep track of the fragments with mouse handler, for later use in - # `mouse_handler`. - self._fragments = fragments_with_mouse_handlers - - # If there is a `[SetCursorPosition]` in the fragment list, set the - # cursor position here. - def get_cursor_position( - fragment: str = "[SetCursorPosition]", - ) -> Optional[Point]: - for y, line in enumerate(fragment_lines): - x = 0 - for style_str, text, *_ in line: - if fragment in style_str: - return Point(x=x, y=y) - x += len(text) - return None - - # If there is a `[SetMenuPosition]`, set the menu over here. - def get_menu_position() -> Optional[Point]: - return get_cursor_position("[SetMenuPosition]") - - cursor_position = (self.get_cursor_position or get_cursor_position)() - - # Create content, or take it from the cache. - key = (tuple(fragments_with_mouse_handlers), width, cursor_position) - - def get_content() -> UIContent: - return UIContent( - get_line=lambda i: fragment_lines[i], - line_count=len(fragment_lines), - show_cursor=self.show_cursor, - cursor_position=cursor_position, - menu_position=get_menu_position(), - ) - - return self._content_cache.get(key, get_content) - - def mouse_handler(self, mouse_event: MouseEvent) -> "NotImplementedOrNone": - """ - Handle mouse events. - - (When the fragment list contained mouse handlers and the user clicked on - on any of these, the matching handler is called. This handler can still - return `NotImplemented` in case we want the - :class:`~prompt_toolkit.layout.Window` to handle this particular - event.) - """ - if self._fragments: - # Read the generator. - fragments_for_line = list(split_lines(self._fragments)) - - try: - fragments = fragments_for_line[mouse_event.position.y] - except IndexError: - return NotImplemented - else: - # Find position in the fragment list. - xpos = mouse_event.position.x - - # Find mouse handler for this character. - count = 0 - for item in fragments: - count += len(item[1]) - if count > xpos: - if len(item) >= 3: - # Handler found. Call it. - # (Handler can return NotImplemented, so return - # that result.) - handler = item[2] # type: ignore - return handler(mouse_event) - else: - break - - # Otherwise, don't handle here. - return NotImplemented - - def is_modal(self) -> bool: - return self.modal - - def get_key_bindings(self) -> Optional["KeyBindingsBase"]: - return self.key_bindings - - -class DummyControl(UIControl): - """ - A dummy control object that doesn't paint any content. - - Useful for filling a :class:`~prompt_toolkit.layout.Window`. (The - `fragment` and `char` attributes of the `Window` class can be used to - define the filling.) - """ - - def create_content(self, width: int, height: int) -> UIContent: - def get_line(i: int) -> StyleAndTextTuples: - return [] - - return UIContent( + else: + if remainder: + quotient += 1 # Like math.ceil. + height = max(1, quotient) + + # Cache and return + self._line_heights_cache[key] = height + return height + + +class FormattedTextControl(UIControl): + """ + Control that displays formatted text. This can be either plain text, an + :class:`~prompt_toolkit.formatted_text.HTML` object an + :class:`~prompt_toolkit.formatted_text.ANSI` object, a list of ``(style_str, + text)`` tuples or a callable that takes no argument and returns one of + those, depending on how you prefer to do the formatting. See + ``prompt_toolkit.layout.formatted_text`` for more information. + + (It's mostly optimized for rather small widgets, like toolbars, menus, etc...) + + When this UI control has the focus, the cursor will be shown in the upper + left corner of this control by default. There are two ways for specifying + the cursor position: + + - Pass a `get_cursor_position` function which returns a `Point` instance + with the current cursor position. + + - If the (formatted) text is passed as a list of ``(style, text)`` tuples + and there is one that looks like ``('[SetCursorPosition]', '')``, then + this will specify the cursor position. + + Mouse support: + + The list of fragments can also contain tuples of three items, looking like: + (style_str, text, handler). When mouse support is enabled and the user + clicks on this fragment, then the given handler is called. That handler + should accept two inputs: (Application, MouseEvent) and it should + either handle the event or return `NotImplemented` in case we want the + containing Window to handle this event. + + :param focusable: `bool` or :class:`.Filter`: Tell whether this control is + focusable. + + :param text: Text or formatted text to be displayed. + :param style: Style string applied to the content. (If you want to style + the whole :class:`~prompt_toolkit.layout.Window`, pass the style to the + :class:`~prompt_toolkit.layout.Window` instead.) + :param key_bindings: a :class:`.KeyBindings` object. + :param get_cursor_position: A callable that returns the cursor position as + a `Point` instance. + """ + + def __init__( + self, + text: AnyFormattedText = "", + style: str = "", + focusable: FilterOrBool = False, + key_bindings: Optional["KeyBindingsBase"] = None, + show_cursor: bool = True, + modal: bool = False, + get_cursor_position: Optional[Callable[[], Optional[Point]]] = None, + ) -> None: + + self.text = text # No type check on 'text'. This is done dynamically. + self.style = style + self.focusable = to_filter(focusable) + + # Key bindings. + self.key_bindings = key_bindings + self.show_cursor = show_cursor + self.modal = modal + self.get_cursor_position = get_cursor_position + + #: Cache for the content. + self._content_cache: SimpleCache[Hashable, UIContent] = SimpleCache(maxsize=18) + self._fragment_cache: SimpleCache[int, StyleAndTextTuples] = SimpleCache( + maxsize=1 + ) + # Only cache one fragment list. We don't need the previous item. + + # Render info for the mouse support. + self._fragments: Optional[StyleAndTextTuples] = None + + def reset(self) -> None: + self._fragments = None + + def is_focusable(self) -> bool: + return self.focusable() + + def __repr__(self) -> str: + return "%s(%r)" % (self.__class__.__name__, self.text) + + def _get_formatted_text_cached(self) -> StyleAndTextTuples: + """ + Get fragments, but only retrieve fragments once during one render run. + (This function is called several times during one rendering, because + we also need those for calculating the dimensions.) + """ + return self._fragment_cache.get( + get_app().render_counter, lambda: to_formatted_text(self.text, self.style) + ) + + def preferred_width(self, max_available_width: int) -> int: + """ + Return the preferred width for this control. + That is the width of the longest line. + """ + text = fragment_list_to_text(self._get_formatted_text_cached()) + line_lengths = [get_cwidth(l) for l in text.split("\n")] + return max(line_lengths) + + def preferred_height( + self, + width: int, + max_available_height: int, + wrap_lines: bool, + get_line_prefix: Optional[GetLinePrefixCallable], + ) -> Optional[int]: + """ + Return the preferred height for this control. + """ + content = self.create_content(width, None) + if wrap_lines: + height = 0 + for i in range(content.line_count): + height += content.get_height_for_line(i, width, get_line_prefix) + if height >= max_available_height: + return max_available_height + return height + else: + return content.line_count + + def create_content(self, width: int, height: Optional[int]) -> UIContent: + # Get fragments + fragments_with_mouse_handlers = self._get_formatted_text_cached() + fragment_lines_with_mouse_handlers = list( + split_lines(fragments_with_mouse_handlers) + ) + + # Strip mouse handlers from fragments. + fragment_lines: List[StyleAndTextTuples] = [ + [(item[0], item[1]) for item in line] + for line in fragment_lines_with_mouse_handlers + ] + + # Keep track of the fragments with mouse handler, for later use in + # `mouse_handler`. + self._fragments = fragments_with_mouse_handlers + + # If there is a `[SetCursorPosition]` in the fragment list, set the + # cursor position here. + def get_cursor_position( + fragment: str = "[SetCursorPosition]", + ) -> Optional[Point]: + for y, line in enumerate(fragment_lines): + x = 0 + for style_str, text, *_ in line: + if fragment in style_str: + return Point(x=x, y=y) + x += len(text) + return None + + # If there is a `[SetMenuPosition]`, set the menu over here. + def get_menu_position() -> Optional[Point]: + return get_cursor_position("[SetMenuPosition]") + + cursor_position = (self.get_cursor_position or get_cursor_position)() + + # Create content, or take it from the cache. + key = (tuple(fragments_with_mouse_handlers), width, cursor_position) + + def get_content() -> UIContent: + return UIContent( + get_line=lambda i: fragment_lines[i], + line_count=len(fragment_lines), + show_cursor=self.show_cursor, + cursor_position=cursor_position, + menu_position=get_menu_position(), + ) + + return self._content_cache.get(key, get_content) + + def mouse_handler(self, mouse_event: MouseEvent) -> "NotImplementedOrNone": + """ + Handle mouse events. + + (When the fragment list contained mouse handlers and the user clicked on + on any of these, the matching handler is called. This handler can still + return `NotImplemented` in case we want the + :class:`~prompt_toolkit.layout.Window` to handle this particular + event.) + """ + if self._fragments: + # Read the generator. + fragments_for_line = list(split_lines(self._fragments)) + + try: + fragments = fragments_for_line[mouse_event.position.y] + except IndexError: + return NotImplemented + else: + # Find position in the fragment list. + xpos = mouse_event.position.x + + # Find mouse handler for this character. + count = 0 + for item in fragments: + count += len(item[1]) + if count > xpos: + if len(item) >= 3: + # Handler found. Call it. + # (Handler can return NotImplemented, so return + # that result.) + handler = item[2] # type: ignore + return handler(mouse_event) + else: + break + + # Otherwise, don't handle here. + return NotImplemented + + def is_modal(self) -> bool: + return self.modal + + def get_key_bindings(self) -> Optional["KeyBindingsBase"]: + return self.key_bindings + + +class DummyControl(UIControl): + """ + A dummy control object that doesn't paint any content. + + Useful for filling a :class:`~prompt_toolkit.layout.Window`. (The + `fragment` and `char` attributes of the `Window` class can be used to + define the filling.) + """ + + def create_content(self, width: int, height: int) -> UIContent: + def get_line(i: int) -> StyleAndTextTuples: + return [] + + return UIContent( get_line=get_line, line_count=100**100 - ) # Something very big. - - def is_focusable(self) -> bool: - return False - - -_ProcessedLine = NamedTuple( - "_ProcessedLine", - [ - ("fragments", StyleAndTextTuples), - ("source_to_display", Callable[[int], int]), - ("display_to_source", Callable[[int], int]), - ], -) - - -class BufferControl(UIControl): - """ - Control for visualising the content of a :class:`.Buffer`. - - :param buffer: The :class:`.Buffer` object to be displayed. - :param input_processors: A list of - :class:`~prompt_toolkit.layout.processors.Processor` objects. - :param include_default_input_processors: When True, include the default - processors for highlighting of selection, search and displaying of - multiple cursors. - :param lexer: :class:`.Lexer` instance for syntax highlighting. - :param preview_search: `bool` or :class:`.Filter`: Show search while - typing. When this is `True`, probably you want to add a - ``HighlightIncrementalSearchProcessor`` as well. Otherwise only the - cursor position will move, but the text won't be highlighted. - :param focusable: `bool` or :class:`.Filter`: Tell whether this control is focusable. - :param focus_on_click: Focus this buffer when it's click, but not yet focused. - :param key_bindings: a :class:`.KeyBindings` object. - """ - - def __init__( - self, - buffer: Optional[Buffer] = None, - input_processors: Optional[List[Processor]] = None, - include_default_input_processors: bool = True, - lexer: Optional[Lexer] = None, - preview_search: FilterOrBool = False, - focusable: FilterOrBool = True, - search_buffer_control: Union[ - None, "SearchBufferControl", Callable[[], "SearchBufferControl"] - ] = None, - menu_position: Optional[Callable[[], Optional[int]]] = None, - focus_on_click: FilterOrBool = False, - key_bindings: Optional["KeyBindingsBase"] = None, - ): - - self.input_processors = input_processors - self.include_default_input_processors = include_default_input_processors - - self.default_input_processors = [ - HighlightSearchProcessor(), - HighlightIncrementalSearchProcessor(), - HighlightSelectionProcessor(), - DisplayMultipleCursors(), - ] - - self.preview_search = to_filter(preview_search) - self.focusable = to_filter(focusable) - self.focus_on_click = to_filter(focus_on_click) - - self.buffer = buffer or Buffer() - self.menu_position = menu_position - self.lexer = lexer or SimpleLexer() - self.key_bindings = key_bindings - self._search_buffer_control = search_buffer_control - - #: Cache for the lexer. - #: Often, due to cursor movement, undo/redo and window resizing - #: operations, it happens that a short time, the same document has to be - #: lexed. This is a fairly easy way to cache such an expensive operation. - self._fragment_cache: SimpleCache[ - Hashable, Callable[[int], StyleAndTextTuples] - ] = SimpleCache(maxsize=8) - - self._last_click_timestamp: Optional[float] = None - self._last_get_processed_line: Optional[Callable[[int], _ProcessedLine]] = None - - def __repr__(self) -> str: - return "<%s buffer=%r at %r>" % (self.__class__.__name__, self.buffer, id(self)) - - @property - def search_buffer_control(self) -> Optional["SearchBufferControl"]: - result: Optional[SearchBufferControl] - - if callable(self._search_buffer_control): - result = self._search_buffer_control() - else: - result = self._search_buffer_control - - assert result is None or isinstance(result, SearchBufferControl) - return result - - @property - def search_buffer(self) -> Optional[Buffer]: - control = self.search_buffer_control - if control is not None: - return control.buffer - return None - - @property - def search_state(self) -> SearchState: - """ - Return the `SearchState` for searching this `BufferControl`. This is - always associated with the search control. If one search bar is used - for searching multiple `BufferControls`, then they share the same - `SearchState`. - """ - search_buffer_control = self.search_buffer_control - if search_buffer_control: - return search_buffer_control.searcher_search_state - else: - return SearchState() - - def is_focusable(self) -> bool: - return self.focusable() - - def preferred_width(self, max_available_width: int) -> Optional[int]: - """ - This should return the preferred width. - - Note: We don't specify a preferred width according to the content, - because it would be too expensive. Calculating the preferred - width can be done by calculating the longest line, but this would - require applying all the processors to each line. This is - unfeasible for a larger document, and doing it for small - documents only would result in inconsistent behaviour. - """ - return None - - def preferred_height( - self, - width: int, - max_available_height: int, - wrap_lines: bool, - get_line_prefix: Optional[GetLinePrefixCallable], - ) -> Optional[int]: - - # Calculate the content height, if it was drawn on a screen with the - # given width. - height = 0 - content = self.create_content(width, height=1) # Pass a dummy '1' as height. - - # When line wrapping is off, the height should be equal to the amount - # of lines. - if not wrap_lines: - return content.line_count - - # When the number of lines exceeds the max_available_height, just - # return max_available_height. No need to calculate anything. - if content.line_count >= max_available_height: - return max_available_height - - for i in range(content.line_count): - height += content.get_height_for_line(i, width, get_line_prefix) - - if height >= max_available_height: - return max_available_height - - return height - - def _get_formatted_text_for_line_func( - self, document: Document - ) -> Callable[[int], StyleAndTextTuples]: - """ - Create a function that returns the fragments for a given line. - """ - # Cache using `document.text`. - def get_formatted_text_for_line() -> Callable[[int], StyleAndTextTuples]: - return self.lexer.lex_document(document) - - key = (document.text, self.lexer.invalidation_hash()) - return self._fragment_cache.get(key, get_formatted_text_for_line) - - def _create_get_processed_line_func( - self, document: Document, width: int, height: int - ) -> Callable[[int], _ProcessedLine]: - """ - Create a function that takes a line number of the current document and - returns a _ProcessedLine(processed_fragments, source_to_display, display_to_source) - tuple. - """ - # Merge all input processors together. - input_processors = self.input_processors or [] - if self.include_default_input_processors: - input_processors = self.default_input_processors + input_processors - - merged_processor = merge_processors(input_processors) - - def transform(lineno: int, fragments: StyleAndTextTuples) -> _ProcessedLine: - "Transform the fragments for a given line number." - # Get cursor position at this line. - def source_to_display(i: int) -> int: - """X position from the buffer to the x position in the - processed fragment list. By default, we start from the 'identity' - operation.""" - return i - - transformation = merged_processor.apply_transformation( - TransformationInput( - self, document, lineno, source_to_display, fragments, width, height - ) - ) - - return _ProcessedLine( - transformation.fragments, - transformation.source_to_display, - transformation.display_to_source, - ) - - def create_func() -> Callable[[int], _ProcessedLine]: - get_line = self._get_formatted_text_for_line_func(document) - cache: Dict[int, _ProcessedLine] = {} - - def get_processed_line(i: int) -> _ProcessedLine: - try: - return cache[i] - except KeyError: - processed_line = transform(i, get_line(i)) - cache[i] = processed_line - return processed_line - - return get_processed_line - - return create_func() - - def create_content( - self, width: int, height: int, preview_search: bool = False - ) -> UIContent: - """ - Create a UIContent. - """ - buffer = self.buffer - - # Trigger history loading of the buffer. We do this during the - # rendering of the UI here, because it needs to happen when an - # `Application` with its event loop is running. During the rendering of - # the buffer control is the earliest place we can achieve this, where - # we're sure the right event loop is active, and don't require user - # interaction (like in a key binding). - buffer.load_history_if_not_yet_loaded() - - # Get the document to be shown. If we are currently searching (the - # search buffer has focus, and the preview_search filter is enabled), - # then use the search document, which has possibly a different - # text/cursor position.) - search_control = self.search_buffer_control - preview_now = preview_search or bool( - # Only if this feature is enabled. - self.preview_search() - and - # And something was typed in the associated search field. - search_control - and search_control.buffer.text - and - # And we are searching in this control. (Many controls can point to - # the same search field, like in Pyvim.) - get_app().layout.search_target_buffer_control == self - ) - - if preview_now and search_control is not None: - ss = self.search_state - - document = buffer.document_for_search( - SearchState( - text=search_control.buffer.text, - direction=ss.direction, - ignore_case=ss.ignore_case, - ) - ) - else: - document = buffer.document - - get_processed_line = self._create_get_processed_line_func( - document, width, height - ) - self._last_get_processed_line = get_processed_line - - def translate_rowcol(row: int, col: int) -> Point: - "Return the content column for this coordinate." - return Point(x=get_processed_line(row).source_to_display(col), y=row) - - def get_line(i: int) -> StyleAndTextTuples: - "Return the fragments for a given line number." - fragments = get_processed_line(i).fragments - - # Add a space at the end, because that is a possible cursor - # position. (When inserting after the input.) We should do this on - # all the lines, not just the line containing the cursor. (Because - # otherwise, line wrapping/scrolling could change when moving the - # cursor around.) - fragments = fragments + [("", " ")] - return fragments - - content = UIContent( - get_line=get_line, - line_count=document.line_count, - cursor_position=translate_rowcol( - document.cursor_position_row, document.cursor_position_col - ), - ) - - # If there is an auto completion going on, use that start point for a - # pop-up menu position. (But only when this buffer has the focus -- - # there is only one place for a menu, determined by the focused buffer.) - if get_app().layout.current_control == self: - menu_position = self.menu_position() if self.menu_position else None - if menu_position is not None: - assert isinstance(menu_position, int) - menu_row, menu_col = buffer.document.translate_index_to_position( - menu_position - ) - content.menu_position = translate_rowcol(menu_row, menu_col) - elif buffer.complete_state: - # Position for completion menu. - # Note: We use 'min', because the original cursor position could be - # behind the input string when the actual completion is for - # some reason shorter than the text we had before. (A completion - # can change and shorten the input.) - menu_row, menu_col = buffer.document.translate_index_to_position( - min( - buffer.cursor_position, - buffer.complete_state.original_document.cursor_position, - ) - ) - content.menu_position = translate_rowcol(menu_row, menu_col) - else: - content.menu_position = None - - return content - - def mouse_handler(self, mouse_event: MouseEvent) -> "NotImplementedOrNone": - """ - Mouse handler for this control. - """ - buffer = self.buffer - position = mouse_event.position - - # Focus buffer when clicked. - if get_app().layout.current_control == self: - if self._last_get_processed_line: - processed_line = self._last_get_processed_line(position.y) - - # Translate coordinates back to the cursor position of the - # original input. - xpos = processed_line.display_to_source(position.x) - index = buffer.document.translate_row_col_to_index(position.y, xpos) - - # Set the cursor position. - if mouse_event.event_type == MouseEventType.MOUSE_DOWN: - buffer.exit_selection() - buffer.cursor_position = index - - elif ( - mouse_event.event_type == MouseEventType.MOUSE_MOVE - and mouse_event.button != MouseButton.NONE - ): - # Click and drag to highlight a selection - if ( - buffer.selection_state is None - and abs(buffer.cursor_position - index) > 0 - ): - buffer.start_selection(selection_type=SelectionType.CHARACTERS) - buffer.cursor_position = index - - elif mouse_event.event_type == MouseEventType.MOUSE_UP: - # When the cursor was moved to another place, select the text. - # (The >1 is actually a small but acceptable workaround for - # selecting text in Vi navigation mode. In navigation mode, - # the cursor can never be after the text, so the cursor - # will be repositioned automatically.) - if abs(buffer.cursor_position - index) > 1: - if buffer.selection_state is None: - buffer.start_selection( - selection_type=SelectionType.CHARACTERS - ) - buffer.cursor_position = index - - # Select word around cursor on double click. - # Two MOUSE_UP events in a short timespan are considered a double click. - double_click = ( - self._last_click_timestamp - and time.time() - self._last_click_timestamp < 0.3 - ) - self._last_click_timestamp = time.time() - - if double_click: - start, end = buffer.document.find_boundaries_of_current_word() - buffer.cursor_position += start - buffer.start_selection(selection_type=SelectionType.CHARACTERS) - buffer.cursor_position += end - start - else: - # Don't handle scroll events here. - return NotImplemented - - # Not focused, but focusing on click events. - else: - if ( - self.focus_on_click() - and mouse_event.event_type == MouseEventType.MOUSE_UP - ): - # Focus happens on mouseup. (If we did this on mousedown, the - # up event will be received at the point where this widget is - # focused and be handled anyway.) - get_app().layout.current_control = self - else: - return NotImplemented - - return None - - def move_cursor_down(self) -> None: - b = self.buffer - b.cursor_position += b.document.get_cursor_down_position() - - def move_cursor_up(self) -> None: - b = self.buffer - b.cursor_position += b.document.get_cursor_up_position() - - def get_key_bindings(self) -> Optional["KeyBindingsBase"]: - """ - When additional key bindings are given. Return these. - """ - return self.key_bindings - - def get_invalidate_events(self) -> Iterable["Event[object]"]: - """ - Return the Window invalidate events. - """ - # Whenever the buffer changes, the UI has to be updated. - yield self.buffer.on_text_changed - yield self.buffer.on_cursor_position_changed - - yield self.buffer.on_completions_changed - yield self.buffer.on_suggestion_set - - -class SearchBufferControl(BufferControl): - """ - :class:`.BufferControl` which is used for searching another - :class:`.BufferControl`. - - :param ignore_case: Search case insensitive. - """ - - def __init__( - self, - buffer: Optional[Buffer] = None, - input_processors: Optional[List[Processor]] = None, - lexer: Optional[Lexer] = None, - focus_on_click: FilterOrBool = False, - key_bindings: Optional["KeyBindingsBase"] = None, - ignore_case: FilterOrBool = False, - ): - - super().__init__( - buffer=buffer, - input_processors=input_processors, - lexer=lexer, - focus_on_click=focus_on_click, - key_bindings=key_bindings, - ) - - # If this BufferControl is used as a search field for one or more other - # BufferControls, then represents the search state. - self.searcher_search_state = SearchState(ignore_case=ignore_case) + ) # Something very big. + + def is_focusable(self) -> bool: + return False + + +_ProcessedLine = NamedTuple( + "_ProcessedLine", + [ + ("fragments", StyleAndTextTuples), + ("source_to_display", Callable[[int], int]), + ("display_to_source", Callable[[int], int]), + ], +) + + +class BufferControl(UIControl): + """ + Control for visualising the content of a :class:`.Buffer`. + + :param buffer: The :class:`.Buffer` object to be displayed. + :param input_processors: A list of + :class:`~prompt_toolkit.layout.processors.Processor` objects. + :param include_default_input_processors: When True, include the default + processors for highlighting of selection, search and displaying of + multiple cursors. + :param lexer: :class:`.Lexer` instance for syntax highlighting. + :param preview_search: `bool` or :class:`.Filter`: Show search while + typing. When this is `True`, probably you want to add a + ``HighlightIncrementalSearchProcessor`` as well. Otherwise only the + cursor position will move, but the text won't be highlighted. + :param focusable: `bool` or :class:`.Filter`: Tell whether this control is focusable. + :param focus_on_click: Focus this buffer when it's click, but not yet focused. + :param key_bindings: a :class:`.KeyBindings` object. + """ + + def __init__( + self, + buffer: Optional[Buffer] = None, + input_processors: Optional[List[Processor]] = None, + include_default_input_processors: bool = True, + lexer: Optional[Lexer] = None, + preview_search: FilterOrBool = False, + focusable: FilterOrBool = True, + search_buffer_control: Union[ + None, "SearchBufferControl", Callable[[], "SearchBufferControl"] + ] = None, + menu_position: Optional[Callable[[], Optional[int]]] = None, + focus_on_click: FilterOrBool = False, + key_bindings: Optional["KeyBindingsBase"] = None, + ): + + self.input_processors = input_processors + self.include_default_input_processors = include_default_input_processors + + self.default_input_processors = [ + HighlightSearchProcessor(), + HighlightIncrementalSearchProcessor(), + HighlightSelectionProcessor(), + DisplayMultipleCursors(), + ] + + self.preview_search = to_filter(preview_search) + self.focusable = to_filter(focusable) + self.focus_on_click = to_filter(focus_on_click) + + self.buffer = buffer or Buffer() + self.menu_position = menu_position + self.lexer = lexer or SimpleLexer() + self.key_bindings = key_bindings + self._search_buffer_control = search_buffer_control + + #: Cache for the lexer. + #: Often, due to cursor movement, undo/redo and window resizing + #: operations, it happens that a short time, the same document has to be + #: lexed. This is a fairly easy way to cache such an expensive operation. + self._fragment_cache: SimpleCache[ + Hashable, Callable[[int], StyleAndTextTuples] + ] = SimpleCache(maxsize=8) + + self._last_click_timestamp: Optional[float] = None + self._last_get_processed_line: Optional[Callable[[int], _ProcessedLine]] = None + + def __repr__(self) -> str: + return "<%s buffer=%r at %r>" % (self.__class__.__name__, self.buffer, id(self)) + + @property + def search_buffer_control(self) -> Optional["SearchBufferControl"]: + result: Optional[SearchBufferControl] + + if callable(self._search_buffer_control): + result = self._search_buffer_control() + else: + result = self._search_buffer_control + + assert result is None or isinstance(result, SearchBufferControl) + return result + + @property + def search_buffer(self) -> Optional[Buffer]: + control = self.search_buffer_control + if control is not None: + return control.buffer + return None + + @property + def search_state(self) -> SearchState: + """ + Return the `SearchState` for searching this `BufferControl`. This is + always associated with the search control. If one search bar is used + for searching multiple `BufferControls`, then they share the same + `SearchState`. + """ + search_buffer_control = self.search_buffer_control + if search_buffer_control: + return search_buffer_control.searcher_search_state + else: + return SearchState() + + def is_focusable(self) -> bool: + return self.focusable() + + def preferred_width(self, max_available_width: int) -> Optional[int]: + """ + This should return the preferred width. + + Note: We don't specify a preferred width according to the content, + because it would be too expensive. Calculating the preferred + width can be done by calculating the longest line, but this would + require applying all the processors to each line. This is + unfeasible for a larger document, and doing it for small + documents only would result in inconsistent behaviour. + """ + return None + + def preferred_height( + self, + width: int, + max_available_height: int, + wrap_lines: bool, + get_line_prefix: Optional[GetLinePrefixCallable], + ) -> Optional[int]: + + # Calculate the content height, if it was drawn on a screen with the + # given width. + height = 0 + content = self.create_content(width, height=1) # Pass a dummy '1' as height. + + # When line wrapping is off, the height should be equal to the amount + # of lines. + if not wrap_lines: + return content.line_count + + # When the number of lines exceeds the max_available_height, just + # return max_available_height. No need to calculate anything. + if content.line_count >= max_available_height: + return max_available_height + + for i in range(content.line_count): + height += content.get_height_for_line(i, width, get_line_prefix) + + if height >= max_available_height: + return max_available_height + + return height + + def _get_formatted_text_for_line_func( + self, document: Document + ) -> Callable[[int], StyleAndTextTuples]: + """ + Create a function that returns the fragments for a given line. + """ + # Cache using `document.text`. + def get_formatted_text_for_line() -> Callable[[int], StyleAndTextTuples]: + return self.lexer.lex_document(document) + + key = (document.text, self.lexer.invalidation_hash()) + return self._fragment_cache.get(key, get_formatted_text_for_line) + + def _create_get_processed_line_func( + self, document: Document, width: int, height: int + ) -> Callable[[int], _ProcessedLine]: + """ + Create a function that takes a line number of the current document and + returns a _ProcessedLine(processed_fragments, source_to_display, display_to_source) + tuple. + """ + # Merge all input processors together. + input_processors = self.input_processors or [] + if self.include_default_input_processors: + input_processors = self.default_input_processors + input_processors + + merged_processor = merge_processors(input_processors) + + def transform(lineno: int, fragments: StyleAndTextTuples) -> _ProcessedLine: + "Transform the fragments for a given line number." + # Get cursor position at this line. + def source_to_display(i: int) -> int: + """X position from the buffer to the x position in the + processed fragment list. By default, we start from the 'identity' + operation.""" + return i + + transformation = merged_processor.apply_transformation( + TransformationInput( + self, document, lineno, source_to_display, fragments, width, height + ) + ) + + return _ProcessedLine( + transformation.fragments, + transformation.source_to_display, + transformation.display_to_source, + ) + + def create_func() -> Callable[[int], _ProcessedLine]: + get_line = self._get_formatted_text_for_line_func(document) + cache: Dict[int, _ProcessedLine] = {} + + def get_processed_line(i: int) -> _ProcessedLine: + try: + return cache[i] + except KeyError: + processed_line = transform(i, get_line(i)) + cache[i] = processed_line + return processed_line + + return get_processed_line + + return create_func() + + def create_content( + self, width: int, height: int, preview_search: bool = False + ) -> UIContent: + """ + Create a UIContent. + """ + buffer = self.buffer + + # Trigger history loading of the buffer. We do this during the + # rendering of the UI here, because it needs to happen when an + # `Application` with its event loop is running. During the rendering of + # the buffer control is the earliest place we can achieve this, where + # we're sure the right event loop is active, and don't require user + # interaction (like in a key binding). + buffer.load_history_if_not_yet_loaded() + + # Get the document to be shown. If we are currently searching (the + # search buffer has focus, and the preview_search filter is enabled), + # then use the search document, which has possibly a different + # text/cursor position.) + search_control = self.search_buffer_control + preview_now = preview_search or bool( + # Only if this feature is enabled. + self.preview_search() + and + # And something was typed in the associated search field. + search_control + and search_control.buffer.text + and + # And we are searching in this control. (Many controls can point to + # the same search field, like in Pyvim.) + get_app().layout.search_target_buffer_control == self + ) + + if preview_now and search_control is not None: + ss = self.search_state + + document = buffer.document_for_search( + SearchState( + text=search_control.buffer.text, + direction=ss.direction, + ignore_case=ss.ignore_case, + ) + ) + else: + document = buffer.document + + get_processed_line = self._create_get_processed_line_func( + document, width, height + ) + self._last_get_processed_line = get_processed_line + + def translate_rowcol(row: int, col: int) -> Point: + "Return the content column for this coordinate." + return Point(x=get_processed_line(row).source_to_display(col), y=row) + + def get_line(i: int) -> StyleAndTextTuples: + "Return the fragments for a given line number." + fragments = get_processed_line(i).fragments + + # Add a space at the end, because that is a possible cursor + # position. (When inserting after the input.) We should do this on + # all the lines, not just the line containing the cursor. (Because + # otherwise, line wrapping/scrolling could change when moving the + # cursor around.) + fragments = fragments + [("", " ")] + return fragments + + content = UIContent( + get_line=get_line, + line_count=document.line_count, + cursor_position=translate_rowcol( + document.cursor_position_row, document.cursor_position_col + ), + ) + + # If there is an auto completion going on, use that start point for a + # pop-up menu position. (But only when this buffer has the focus -- + # there is only one place for a menu, determined by the focused buffer.) + if get_app().layout.current_control == self: + menu_position = self.menu_position() if self.menu_position else None + if menu_position is not None: + assert isinstance(menu_position, int) + menu_row, menu_col = buffer.document.translate_index_to_position( + menu_position + ) + content.menu_position = translate_rowcol(menu_row, menu_col) + elif buffer.complete_state: + # Position for completion menu. + # Note: We use 'min', because the original cursor position could be + # behind the input string when the actual completion is for + # some reason shorter than the text we had before. (A completion + # can change and shorten the input.) + menu_row, menu_col = buffer.document.translate_index_to_position( + min( + buffer.cursor_position, + buffer.complete_state.original_document.cursor_position, + ) + ) + content.menu_position = translate_rowcol(menu_row, menu_col) + else: + content.menu_position = None + + return content + + def mouse_handler(self, mouse_event: MouseEvent) -> "NotImplementedOrNone": + """ + Mouse handler for this control. + """ + buffer = self.buffer + position = mouse_event.position + + # Focus buffer when clicked. + if get_app().layout.current_control == self: + if self._last_get_processed_line: + processed_line = self._last_get_processed_line(position.y) + + # Translate coordinates back to the cursor position of the + # original input. + xpos = processed_line.display_to_source(position.x) + index = buffer.document.translate_row_col_to_index(position.y, xpos) + + # Set the cursor position. + if mouse_event.event_type == MouseEventType.MOUSE_DOWN: + buffer.exit_selection() + buffer.cursor_position = index + + elif ( + mouse_event.event_type == MouseEventType.MOUSE_MOVE + and mouse_event.button != MouseButton.NONE + ): + # Click and drag to highlight a selection + if ( + buffer.selection_state is None + and abs(buffer.cursor_position - index) > 0 + ): + buffer.start_selection(selection_type=SelectionType.CHARACTERS) + buffer.cursor_position = index + + elif mouse_event.event_type == MouseEventType.MOUSE_UP: + # When the cursor was moved to another place, select the text. + # (The >1 is actually a small but acceptable workaround for + # selecting text in Vi navigation mode. In navigation mode, + # the cursor can never be after the text, so the cursor + # will be repositioned automatically.) + if abs(buffer.cursor_position - index) > 1: + if buffer.selection_state is None: + buffer.start_selection( + selection_type=SelectionType.CHARACTERS + ) + buffer.cursor_position = index + + # Select word around cursor on double click. + # Two MOUSE_UP events in a short timespan are considered a double click. + double_click = ( + self._last_click_timestamp + and time.time() - self._last_click_timestamp < 0.3 + ) + self._last_click_timestamp = time.time() + + if double_click: + start, end = buffer.document.find_boundaries_of_current_word() + buffer.cursor_position += start + buffer.start_selection(selection_type=SelectionType.CHARACTERS) + buffer.cursor_position += end - start + else: + # Don't handle scroll events here. + return NotImplemented + + # Not focused, but focusing on click events. + else: + if ( + self.focus_on_click() + and mouse_event.event_type == MouseEventType.MOUSE_UP + ): + # Focus happens on mouseup. (If we did this on mousedown, the + # up event will be received at the point where this widget is + # focused and be handled anyway.) + get_app().layout.current_control = self + else: + return NotImplemented + + return None + + def move_cursor_down(self) -> None: + b = self.buffer + b.cursor_position += b.document.get_cursor_down_position() + + def move_cursor_up(self) -> None: + b = self.buffer + b.cursor_position += b.document.get_cursor_up_position() + + def get_key_bindings(self) -> Optional["KeyBindingsBase"]: + """ + When additional key bindings are given. Return these. + """ + return self.key_bindings + + def get_invalidate_events(self) -> Iterable["Event[object]"]: + """ + Return the Window invalidate events. + """ + # Whenever the buffer changes, the UI has to be updated. + yield self.buffer.on_text_changed + yield self.buffer.on_cursor_position_changed + + yield self.buffer.on_completions_changed + yield self.buffer.on_suggestion_set + + +class SearchBufferControl(BufferControl): + """ + :class:`.BufferControl` which is used for searching another + :class:`.BufferControl`. + + :param ignore_case: Search case insensitive. + """ + + def __init__( + self, + buffer: Optional[Buffer] = None, + input_processors: Optional[List[Processor]] = None, + lexer: Optional[Lexer] = None, + focus_on_click: FilterOrBool = False, + key_bindings: Optional["KeyBindingsBase"] = None, + ignore_case: FilterOrBool = False, + ): + + super().__init__( + buffer=buffer, + input_processors=input_processors, + lexer=lexer, + focus_on_click=focus_on_click, + key_bindings=key_bindings, + ) + + # If this BufferControl is used as a search field for one or more other + # BufferControls, then represents the search state. + self.searcher_search_state = SearchState(ignore_case=ignore_case) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/dimension.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/dimension.py index 128e51388f..04c21637cb 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/dimension.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/dimension.py @@ -1,217 +1,217 @@ -""" -Layout dimensions are used to give the minimum, maximum and preferred -dimensions for containers and controls. -""" -from typing import TYPE_CHECKING, Any, Callable, List, Optional, Union - -__all__ = [ - "Dimension", - "D", - "sum_layout_dimensions", - "max_layout_dimensions", - "AnyDimension", - "to_dimension", - "is_dimension", -] - -if TYPE_CHECKING: - from typing_extensions import TypeGuard - - -class Dimension: - """ - Specified dimension (width/height) of a user control or window. - - The layout engine tries to honor the preferred size. If that is not - possible, because the terminal is larger or smaller, it tries to keep in - between min and max. - - :param min: Minimum size. - :param max: Maximum size. - :param weight: For a VSplit/HSplit, the actual size will be determined - by taking the proportion of weights from all the children. - E.g. When there are two children, one with a weight of 1, - and the other with a weight of 2, the second will always be - twice as big as the first, if the min/max values allow it. - :param preferred: Preferred size. - """ - - def __init__( - self, - min: Optional[int] = None, - max: Optional[int] = None, - weight: Optional[int] = None, - preferred: Optional[int] = None, - ) -> None: - if weight is not None: - assert weight >= 0 # Also cannot be a float. - - assert min is None or min >= 0 - assert max is None or max >= 0 - assert preferred is None or preferred >= 0 - - self.min_specified = min is not None - self.max_specified = max is not None - self.preferred_specified = preferred is not None - self.weight_specified = weight is not None - - if min is None: - min = 0 # Smallest possible value. - if max is None: # 0-values are allowed, so use "is None" +""" +Layout dimensions are used to give the minimum, maximum and preferred +dimensions for containers and controls. +""" +from typing import TYPE_CHECKING, Any, Callable, List, Optional, Union + +__all__ = [ + "Dimension", + "D", + "sum_layout_dimensions", + "max_layout_dimensions", + "AnyDimension", + "to_dimension", + "is_dimension", +] + +if TYPE_CHECKING: + from typing_extensions import TypeGuard + + +class Dimension: + """ + Specified dimension (width/height) of a user control or window. + + The layout engine tries to honor the preferred size. If that is not + possible, because the terminal is larger or smaller, it tries to keep in + between min and max. + + :param min: Minimum size. + :param max: Maximum size. + :param weight: For a VSplit/HSplit, the actual size will be determined + by taking the proportion of weights from all the children. + E.g. When there are two children, one with a weight of 1, + and the other with a weight of 2, the second will always be + twice as big as the first, if the min/max values allow it. + :param preferred: Preferred size. + """ + + def __init__( + self, + min: Optional[int] = None, + max: Optional[int] = None, + weight: Optional[int] = None, + preferred: Optional[int] = None, + ) -> None: + if weight is not None: + assert weight >= 0 # Also cannot be a float. + + assert min is None or min >= 0 + assert max is None or max >= 0 + assert preferred is None or preferred >= 0 + + self.min_specified = min is not None + self.max_specified = max is not None + self.preferred_specified = preferred is not None + self.weight_specified = weight is not None + + if min is None: + min = 0 # Smallest possible value. + if max is None: # 0-values are allowed, so use "is None" max = 1000**10 # Something huge. - if preferred is None: - preferred = min - if weight is None: - weight = 1 - - self.min = min - self.max = max - self.preferred = preferred - self.weight = weight - - # Don't allow situations where max < min. (This would be a bug.) - if max < min: - raise ValueError("Invalid Dimension: max < min.") - - # Make sure that the 'preferred' size is always in the min..max range. - if self.preferred < self.min: - self.preferred = self.min - - if self.preferred > self.max: - self.preferred = self.max - - @classmethod - def exact(cls, amount: int) -> "Dimension": - """ - Return a :class:`.Dimension` with an exact size. (min, max and - preferred set to ``amount``). - """ - return cls(min=amount, max=amount, preferred=amount) - - @classmethod - def zero(cls) -> "Dimension": - """ - Create a dimension that represents a zero size. (Used for 'invisible' - controls.) - """ - return cls.exact(amount=0) - - def is_zero(self) -> bool: - "True if this `Dimension` represents a zero size." - return self.preferred == 0 or self.max == 0 - - def __repr__(self) -> str: - fields = [] - if self.min_specified: - fields.append("min=%r" % self.min) - if self.max_specified: - fields.append("max=%r" % self.max) - if self.preferred_specified: - fields.append("preferred=%r" % self.preferred) - if self.weight_specified: - fields.append("weight=%r" % self.weight) - - return "Dimension(%s)" % ", ".join(fields) - - -def sum_layout_dimensions(dimensions: List[Dimension]) -> Dimension: - """ - Sum a list of :class:`.Dimension` instances. - """ - min = sum(d.min for d in dimensions) - max = sum(d.max for d in dimensions) - preferred = sum(d.preferred for d in dimensions) - - return Dimension(min=min, max=max, preferred=preferred) - - -def max_layout_dimensions(dimensions: List[Dimension]) -> Dimension: - """ - Take the maximum of a list of :class:`.Dimension` instances. - Used when we have a HSplit/VSplit, and we want to get the best width/height.) - """ - if not len(dimensions): - return Dimension.zero() - - # If all dimensions are size zero. Return zero. - # (This is important for HSplit/VSplit, to report the right values to their - # parent when all children are invisible.) - if all(d.is_zero() for d in dimensions): - return dimensions[0] - - # Ignore empty dimensions. (They should not reduce the size of others.) - dimensions = [d for d in dimensions if not d.is_zero()] - - if dimensions: - # Take the highest minimum dimension. - min_ = max(d.min for d in dimensions) - - # For the maximum, we would prefer not to go larger than then smallest - # 'max' value, unless other dimensions have a bigger preferred value. - # This seems to work best: - # - We don't want that a widget with a small height in a VSplit would - # shrink other widgets in the split. - # If it doesn't work well enough, then it's up to the UI designer to - # explicitly pass dimensions. - max_ = min(d.max for d in dimensions) - max_ = max(max_, max(d.preferred for d in dimensions)) - - # Make sure that min>=max. In some scenarios, when certain min..max - # ranges don't have any overlap, we can end up in such an impossible - # situation. In that case, give priority to the max value. - # E.g. taking (1..5) and (8..9) would return (8..5). Instead take (8..8). - if min_ > max_: - max_ = min_ - - preferred = max(d.preferred for d in dimensions) - - return Dimension(min=min_, max=max_, preferred=preferred) - else: - return Dimension() - - -# Anything that can be converted to a dimension. -AnyDimension = Union[ - None, # None is a valid dimension that will fit anything. - int, - Dimension, - # Callable[[], 'AnyDimension'] # Recursive definition not supported by mypy. - Callable[[], Any], -] - - -def to_dimension(value: AnyDimension) -> Dimension: - """ - Turn the given object into a `Dimension` object. - """ - if value is None: - return Dimension() - if isinstance(value, int): - return Dimension.exact(value) - if isinstance(value, Dimension): - return value - if callable(value): - return to_dimension(value()) - - raise ValueError("Not an integer or Dimension object.") - - -def is_dimension(value: object) -> "TypeGuard[AnyDimension]": - """ - Test whether the given value could be a valid dimension. - (For usage in an assertion. It's not guaranteed in case of a callable.) - """ - if value is None: - return True - if callable(value): - return True # Assume it's a callable that doesn't take arguments. - if isinstance(value, (int, Dimension)): - return True - return False - - -# Common alias. -D = Dimension - -# For backward-compatibility. -LayoutDimension = Dimension + if preferred is None: + preferred = min + if weight is None: + weight = 1 + + self.min = min + self.max = max + self.preferred = preferred + self.weight = weight + + # Don't allow situations where max < min. (This would be a bug.) + if max < min: + raise ValueError("Invalid Dimension: max < min.") + + # Make sure that the 'preferred' size is always in the min..max range. + if self.preferred < self.min: + self.preferred = self.min + + if self.preferred > self.max: + self.preferred = self.max + + @classmethod + def exact(cls, amount: int) -> "Dimension": + """ + Return a :class:`.Dimension` with an exact size. (min, max and + preferred set to ``amount``). + """ + return cls(min=amount, max=amount, preferred=amount) + + @classmethod + def zero(cls) -> "Dimension": + """ + Create a dimension that represents a zero size. (Used for 'invisible' + controls.) + """ + return cls.exact(amount=0) + + def is_zero(self) -> bool: + "True if this `Dimension` represents a zero size." + return self.preferred == 0 or self.max == 0 + + def __repr__(self) -> str: + fields = [] + if self.min_specified: + fields.append("min=%r" % self.min) + if self.max_specified: + fields.append("max=%r" % self.max) + if self.preferred_specified: + fields.append("preferred=%r" % self.preferred) + if self.weight_specified: + fields.append("weight=%r" % self.weight) + + return "Dimension(%s)" % ", ".join(fields) + + +def sum_layout_dimensions(dimensions: List[Dimension]) -> Dimension: + """ + Sum a list of :class:`.Dimension` instances. + """ + min = sum(d.min for d in dimensions) + max = sum(d.max for d in dimensions) + preferred = sum(d.preferred for d in dimensions) + + return Dimension(min=min, max=max, preferred=preferred) + + +def max_layout_dimensions(dimensions: List[Dimension]) -> Dimension: + """ + Take the maximum of a list of :class:`.Dimension` instances. + Used when we have a HSplit/VSplit, and we want to get the best width/height.) + """ + if not len(dimensions): + return Dimension.zero() + + # If all dimensions are size zero. Return zero. + # (This is important for HSplit/VSplit, to report the right values to their + # parent when all children are invisible.) + if all(d.is_zero() for d in dimensions): + return dimensions[0] + + # Ignore empty dimensions. (They should not reduce the size of others.) + dimensions = [d for d in dimensions if not d.is_zero()] + + if dimensions: + # Take the highest minimum dimension. + min_ = max(d.min for d in dimensions) + + # For the maximum, we would prefer not to go larger than then smallest + # 'max' value, unless other dimensions have a bigger preferred value. + # This seems to work best: + # - We don't want that a widget with a small height in a VSplit would + # shrink other widgets in the split. + # If it doesn't work well enough, then it's up to the UI designer to + # explicitly pass dimensions. + max_ = min(d.max for d in dimensions) + max_ = max(max_, max(d.preferred for d in dimensions)) + + # Make sure that min>=max. In some scenarios, when certain min..max + # ranges don't have any overlap, we can end up in such an impossible + # situation. In that case, give priority to the max value. + # E.g. taking (1..5) and (8..9) would return (8..5). Instead take (8..8). + if min_ > max_: + max_ = min_ + + preferred = max(d.preferred for d in dimensions) + + return Dimension(min=min_, max=max_, preferred=preferred) + else: + return Dimension() + + +# Anything that can be converted to a dimension. +AnyDimension = Union[ + None, # None is a valid dimension that will fit anything. + int, + Dimension, + # Callable[[], 'AnyDimension'] # Recursive definition not supported by mypy. + Callable[[], Any], +] + + +def to_dimension(value: AnyDimension) -> Dimension: + """ + Turn the given object into a `Dimension` object. + """ + if value is None: + return Dimension() + if isinstance(value, int): + return Dimension.exact(value) + if isinstance(value, Dimension): + return value + if callable(value): + return to_dimension(value()) + + raise ValueError("Not an integer or Dimension object.") + + +def is_dimension(value: object) -> "TypeGuard[AnyDimension]": + """ + Test whether the given value could be a valid dimension. + (For usage in an assertion. It's not guaranteed in case of a callable.) + """ + if value is None: + return True + if callable(value): + return True # Assume it's a callable that doesn't take arguments. + if isinstance(value, (int, Dimension)): + return True + return False + + +# Common alias. +D = Dimension + +# For backward-compatibility. +LayoutDimension = Dimension diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/dummy.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/dummy.py index 0ff8195cff..dcd54e9fc9 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/dummy.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/dummy.py @@ -1,37 +1,37 @@ -""" -Dummy layout. Used when somebody creates an `Application` without specifying a -`Layout`. -""" -from prompt_toolkit.formatted_text import HTML -from prompt_toolkit.key_binding import KeyBindings -from prompt_toolkit.key_binding.key_processor import KeyPressEvent - -from .containers import Window -from .controls import FormattedTextControl -from .dimension import D -from .layout import Layout - -__all__ = [ - "create_dummy_layout", -] - -E = KeyPressEvent - - -def create_dummy_layout() -> Layout: - """ - Create a dummy layout for use in an 'Application' that doesn't have a - layout specified. When ENTER is pressed, the application quits. - """ - kb = KeyBindings() - - @kb.add("enter") - def enter(event: E) -> None: - event.app.exit() - - control = FormattedTextControl( - HTML("No layout specified. Press <reverse>ENTER</reverse> to quit."), - key_bindings=kb, - ) - window = Window(content=control, height=D(min=1)) - return Layout(container=window, focused_element=window) +""" +Dummy layout. Used when somebody creates an `Application` without specifying a +`Layout`. +""" +from prompt_toolkit.formatted_text import HTML +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.key_binding.key_processor import KeyPressEvent + +from .containers import Window +from .controls import FormattedTextControl +from .dimension import D +from .layout import Layout + +__all__ = [ + "create_dummy_layout", +] + +E = KeyPressEvent + + +def create_dummy_layout() -> Layout: + """ + Create a dummy layout for use in an 'Application' that doesn't have a + layout specified. When ENTER is pressed, the application quits. + """ + kb = KeyBindings() + + @kb.add("enter") + def enter(event: E) -> None: + event.app.exit() + + control = FormattedTextControl( + HTML("No layout specified. Press <reverse>ENTER</reverse> to quit."), + key_bindings=kb, + ) + window = Window(content=control, height=D(min=1)) + return Layout(container=window, focused_element=window) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/layout.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/layout.py index 6a0acd859e..28bfcb3cac 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/layout.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/layout.py @@ -1,417 +1,417 @@ -""" -Wrapper for the layout. -""" -from typing import Dict, Generator, Iterable, List, Optional, Union - -from prompt_toolkit.buffer import Buffer - -from .containers import ( - AnyContainer, - ConditionalContainer, - Container, - Window, - to_container, -) -from .controls import BufferControl, SearchBufferControl, UIControl - -__all__ = [ - "Layout", - "InvalidLayoutError", - "walk", -] - -FocusableElement = Union[str, Buffer, UIControl, AnyContainer] - - -class Layout: - """ - The layout for a prompt_toolkit - :class:`~prompt_toolkit.application.Application`. - This also keeps track of which user control is focused. - - :param container: The "root" container for the layout. - :param focused_element: element to be focused initially. (Can be anything - the `focus` function accepts.) - """ - - def __init__( - self, - container: AnyContainer, - focused_element: Optional[FocusableElement] = None, - ) -> None: - - self.container = to_container(container) - self._stack: List[Window] = [] - - # Map search BufferControl back to the original BufferControl. - # This is used to keep track of when exactly we are searching, and for - # applying the search. - # When a link exists in this dictionary, that means the search is - # currently active. - # Map: search_buffer_control -> original buffer control. - self.search_links: Dict[SearchBufferControl, BufferControl] = {} - - # Mapping that maps the children in the layout to their parent. - # This relationship is calculated dynamically, each time when the UI - # is rendered. (UI elements have only references to their children.) - self._child_to_parent: Dict[Container, Container] = {} - - if focused_element is None: - try: - self._stack.append(next(self.find_all_windows())) - except StopIteration as e: - raise InvalidLayoutError( - "Invalid layout. The layout does not contain any Window object." - ) from e - else: - self.focus(focused_element) - - # List of visible windows. - self.visible_windows: List[Window] = [] # List of `Window` objects. - - def __repr__(self) -> str: - return "Layout(%r, current_window=%r)" % (self.container, self.current_window) - - def find_all_windows(self) -> Generator[Window, None, None]: - """ - Find all the :class:`.UIControl` objects in this layout. - """ - for item in self.walk(): - if isinstance(item, Window): - yield item - - def find_all_controls(self) -> Iterable[UIControl]: - for container in self.find_all_windows(): - yield container.content - - def focus(self, value: FocusableElement) -> None: - """ - Focus the given UI element. - - `value` can be either: - - - a :class:`.UIControl` - - a :class:`.Buffer` instance or the name of a :class:`.Buffer` - - a :class:`.Window` - - Any container object. In this case we will focus the :class:`.Window` - from this container that was focused most recent, or the very first - focusable :class:`.Window` of the container. - """ - # BufferControl by buffer name. - if isinstance(value, str): - for control in self.find_all_controls(): - if isinstance(control, BufferControl) and control.buffer.name == value: - self.focus(control) - return - raise ValueError( - "Couldn't find Buffer in the current layout: %r." % (value,) - ) - - # BufferControl by buffer object. - elif isinstance(value, Buffer): - for control in self.find_all_controls(): - if isinstance(control, BufferControl) and control.buffer == value: - self.focus(control) - return - raise ValueError( - "Couldn't find Buffer in the current layout: %r." % (value,) - ) - - # Focus UIControl. - elif isinstance(value, UIControl): - if value not in self.find_all_controls(): - raise ValueError( - "Invalid value. Container does not appear in the layout." - ) - if not value.is_focusable(): - raise ValueError("Invalid value. UIControl is not focusable.") - - self.current_control = value - - # Otherwise, expecting any Container object. - else: - value = to_container(value) - - if isinstance(value, Window): - # This is a `Window`: focus that. - if value not in self.find_all_windows(): - raise ValueError( - "Invalid value. Window does not appear in the layout: %r" - % (value,) - ) - - self.current_window = value - else: - # Focus a window in this container. - # If we have many windows as part of this container, and some - # of them have been focused before, take the last focused - # item. (This is very useful when the UI is composed of more - # complex sub components.) - windows = [] - for c in walk(value, skip_hidden=True): - if isinstance(c, Window) and c.content.is_focusable(): - windows.append(c) - - # Take the first one that was focused before. - for w in reversed(self._stack): - if w in windows: - self.current_window = w - return - - # None was focused before: take the very first focusable window. - if windows: - self.current_window = windows[0] - return - - raise ValueError( - "Invalid value. Container cannot be focused: %r" % (value,) - ) - - def has_focus(self, value: FocusableElement) -> bool: - """ - Check whether the given control has the focus. - :param value: :class:`.UIControl` or :class:`.Window` instance. - """ - if isinstance(value, str): - if self.current_buffer is None: - return False - return self.current_buffer.name == value - if isinstance(value, Buffer): - return self.current_buffer == value - if isinstance(value, UIControl): - return self.current_control == value - else: - value = to_container(value) - if isinstance(value, Window): - return self.current_window == value - else: - # Check whether this "container" is focused. This is true if - # one of the elements inside is focused. - for element in walk(value): - if element == self.current_window: - return True - return False - - @property - def current_control(self) -> UIControl: - """ - Get the :class:`.UIControl` to currently has the focus. - """ - return self._stack[-1].content - - @current_control.setter - def current_control(self, control: UIControl) -> None: - """ - Set the :class:`.UIControl` to receive the focus. - """ - for window in self.find_all_windows(): - if window.content == control: - self.current_window = window - return - - raise ValueError("Control not found in the user interface.") - - @property - def current_window(self) -> Window: - "Return the :class:`.Window` object that is currently focused." - return self._stack[-1] - - @current_window.setter - def current_window(self, value: Window) -> None: - "Set the :class:`.Window` object to be currently focused." - self._stack.append(value) - - @property - def is_searching(self) -> bool: - "True if we are searching right now." - return self.current_control in self.search_links - - @property - def search_target_buffer_control(self) -> Optional[BufferControl]: - """ - Return the :class:`.BufferControl` in which we are searching or `None`. - """ - # Not every `UIControl` is a `BufferControl`. This only applies to - # `BufferControl`. - control = self.current_control - - if isinstance(control, SearchBufferControl): - return self.search_links.get(control) - else: - return None - - def get_focusable_windows(self) -> Iterable[Window]: - """ - Return all the :class:`.Window` objects which are focusable (in the - 'modal' area). - """ - for w in self.walk_through_modal_area(): - if isinstance(w, Window) and w.content.is_focusable(): - yield w - - def get_visible_focusable_windows(self) -> List[Window]: - """ - Return a list of :class:`.Window` objects that are focusable. - """ - # focusable windows are windows that are visible, but also part of the - # modal container. Make sure to keep the ordering. - visible_windows = self.visible_windows - return [w for w in self.get_focusable_windows() if w in visible_windows] - - @property - def current_buffer(self) -> Optional[Buffer]: - """ - The currently focused :class:`~.Buffer` or `None`. - """ - ui_control = self.current_control - if isinstance(ui_control, BufferControl): - return ui_control.buffer - return None - - def get_buffer_by_name(self, buffer_name: str) -> Optional[Buffer]: - """ - Look in the layout for a buffer with the given name. - Return `None` when nothing was found. - """ - for w in self.walk(): - if isinstance(w, Window) and isinstance(w.content, BufferControl): - if w.content.buffer.name == buffer_name: - return w.content.buffer - return None - - @property - def buffer_has_focus(self) -> bool: - """ - Return `True` if the currently focused control is a - :class:`.BufferControl`. (For instance, used to determine whether the - default key bindings should be active or not.) - """ - ui_control = self.current_control - return isinstance(ui_control, BufferControl) - - @property - def previous_control(self) -> UIControl: - """ - Get the :class:`.UIControl` to previously had the focus. - """ - try: - return self._stack[-2].content - except IndexError: - return self._stack[-1].content - - def focus_last(self) -> None: - """ - Give the focus to the last focused control. - """ - if len(self._stack) > 1: - self._stack = self._stack[:-1] - - def focus_next(self) -> None: - """ - Focus the next visible/focusable Window. - """ - windows = self.get_visible_focusable_windows() - - if len(windows) > 0: - try: - index = windows.index(self.current_window) - except ValueError: - index = 0 - else: - index = (index + 1) % len(windows) - - self.focus(windows[index]) - - def focus_previous(self) -> None: - """ - Focus the previous visible/focusable Window. - """ - windows = self.get_visible_focusable_windows() - - if len(windows) > 0: - try: - index = windows.index(self.current_window) - except ValueError: - index = 0 - else: - index = (index - 1) % len(windows) - - self.focus(windows[index]) - - def walk(self) -> Iterable[Container]: - """ - Walk through all the layout nodes (and their children) and yield them. - """ - for i in walk(self.container): - yield i - - def walk_through_modal_area(self) -> Iterable[Container]: - """ - Walk through all the containers which are in the current 'modal' part - of the layout. - """ - # Go up in the tree, and find the root. (it will be a part of the - # layout, if the focus is in a modal part.) - root: Container = self.current_window - while not root.is_modal() and root in self._child_to_parent: - root = self._child_to_parent[root] - - for container in walk(root): - yield container - - def update_parents_relations(self) -> None: - """ - Update child->parent relationships mapping. - """ - parents = {} - - def walk(e: Container) -> None: - for c in e.get_children(): - parents[c] = e - walk(c) - - walk(self.container) - - self._child_to_parent = parents - - def reset(self) -> None: - # Remove all search links when the UI starts. - # (Important, for instance when control-c is been pressed while - # searching. The prompt cancels, but next `run()` call the search - # links are still there.) - self.search_links.clear() - - self.container.reset() - - def get_parent(self, container: Container) -> Optional[Container]: - """ - Return the parent container for the given container, or ``None``, if it - wasn't found. - """ - try: - return self._child_to_parent[container] - except KeyError: - return None - - -class InvalidLayoutError(Exception): - pass - - -def walk(container: Container, skip_hidden: bool = False) -> Iterable[Container]: - """ - Walk through layout, starting at this container. - """ - # When `skip_hidden` is set, don't go into disabled ConditionalContainer containers. - if ( - skip_hidden - and isinstance(container, ConditionalContainer) - and not container.filter() - ): - return - - yield container - - for c in container.get_children(): - # yield from walk(c) - yield from walk(c, skip_hidden=skip_hidden) +""" +Wrapper for the layout. +""" +from typing import Dict, Generator, Iterable, List, Optional, Union + +from prompt_toolkit.buffer import Buffer + +from .containers import ( + AnyContainer, + ConditionalContainer, + Container, + Window, + to_container, +) +from .controls import BufferControl, SearchBufferControl, UIControl + +__all__ = [ + "Layout", + "InvalidLayoutError", + "walk", +] + +FocusableElement = Union[str, Buffer, UIControl, AnyContainer] + + +class Layout: + """ + The layout for a prompt_toolkit + :class:`~prompt_toolkit.application.Application`. + This also keeps track of which user control is focused. + + :param container: The "root" container for the layout. + :param focused_element: element to be focused initially. (Can be anything + the `focus` function accepts.) + """ + + def __init__( + self, + container: AnyContainer, + focused_element: Optional[FocusableElement] = None, + ) -> None: + + self.container = to_container(container) + self._stack: List[Window] = [] + + # Map search BufferControl back to the original BufferControl. + # This is used to keep track of when exactly we are searching, and for + # applying the search. + # When a link exists in this dictionary, that means the search is + # currently active. + # Map: search_buffer_control -> original buffer control. + self.search_links: Dict[SearchBufferControl, BufferControl] = {} + + # Mapping that maps the children in the layout to their parent. + # This relationship is calculated dynamically, each time when the UI + # is rendered. (UI elements have only references to their children.) + self._child_to_parent: Dict[Container, Container] = {} + + if focused_element is None: + try: + self._stack.append(next(self.find_all_windows())) + except StopIteration as e: + raise InvalidLayoutError( + "Invalid layout. The layout does not contain any Window object." + ) from e + else: + self.focus(focused_element) + + # List of visible windows. + self.visible_windows: List[Window] = [] # List of `Window` objects. + + def __repr__(self) -> str: + return "Layout(%r, current_window=%r)" % (self.container, self.current_window) + + def find_all_windows(self) -> Generator[Window, None, None]: + """ + Find all the :class:`.UIControl` objects in this layout. + """ + for item in self.walk(): + if isinstance(item, Window): + yield item + + def find_all_controls(self) -> Iterable[UIControl]: + for container in self.find_all_windows(): + yield container.content + + def focus(self, value: FocusableElement) -> None: + """ + Focus the given UI element. + + `value` can be either: + + - a :class:`.UIControl` + - a :class:`.Buffer` instance or the name of a :class:`.Buffer` + - a :class:`.Window` + - Any container object. In this case we will focus the :class:`.Window` + from this container that was focused most recent, or the very first + focusable :class:`.Window` of the container. + """ + # BufferControl by buffer name. + if isinstance(value, str): + for control in self.find_all_controls(): + if isinstance(control, BufferControl) and control.buffer.name == value: + self.focus(control) + return + raise ValueError( + "Couldn't find Buffer in the current layout: %r." % (value,) + ) + + # BufferControl by buffer object. + elif isinstance(value, Buffer): + for control in self.find_all_controls(): + if isinstance(control, BufferControl) and control.buffer == value: + self.focus(control) + return + raise ValueError( + "Couldn't find Buffer in the current layout: %r." % (value,) + ) + + # Focus UIControl. + elif isinstance(value, UIControl): + if value not in self.find_all_controls(): + raise ValueError( + "Invalid value. Container does not appear in the layout." + ) + if not value.is_focusable(): + raise ValueError("Invalid value. UIControl is not focusable.") + + self.current_control = value + + # Otherwise, expecting any Container object. + else: + value = to_container(value) + + if isinstance(value, Window): + # This is a `Window`: focus that. + if value not in self.find_all_windows(): + raise ValueError( + "Invalid value. Window does not appear in the layout: %r" + % (value,) + ) + + self.current_window = value + else: + # Focus a window in this container. + # If we have many windows as part of this container, and some + # of them have been focused before, take the last focused + # item. (This is very useful when the UI is composed of more + # complex sub components.) + windows = [] + for c in walk(value, skip_hidden=True): + if isinstance(c, Window) and c.content.is_focusable(): + windows.append(c) + + # Take the first one that was focused before. + for w in reversed(self._stack): + if w in windows: + self.current_window = w + return + + # None was focused before: take the very first focusable window. + if windows: + self.current_window = windows[0] + return + + raise ValueError( + "Invalid value. Container cannot be focused: %r" % (value,) + ) + + def has_focus(self, value: FocusableElement) -> bool: + """ + Check whether the given control has the focus. + :param value: :class:`.UIControl` or :class:`.Window` instance. + """ + if isinstance(value, str): + if self.current_buffer is None: + return False + return self.current_buffer.name == value + if isinstance(value, Buffer): + return self.current_buffer == value + if isinstance(value, UIControl): + return self.current_control == value + else: + value = to_container(value) + if isinstance(value, Window): + return self.current_window == value + else: + # Check whether this "container" is focused. This is true if + # one of the elements inside is focused. + for element in walk(value): + if element == self.current_window: + return True + return False + + @property + def current_control(self) -> UIControl: + """ + Get the :class:`.UIControl` to currently has the focus. + """ + return self._stack[-1].content + + @current_control.setter + def current_control(self, control: UIControl) -> None: + """ + Set the :class:`.UIControl` to receive the focus. + """ + for window in self.find_all_windows(): + if window.content == control: + self.current_window = window + return + + raise ValueError("Control not found in the user interface.") + + @property + def current_window(self) -> Window: + "Return the :class:`.Window` object that is currently focused." + return self._stack[-1] + + @current_window.setter + def current_window(self, value: Window) -> None: + "Set the :class:`.Window` object to be currently focused." + self._stack.append(value) + + @property + def is_searching(self) -> bool: + "True if we are searching right now." + return self.current_control in self.search_links + + @property + def search_target_buffer_control(self) -> Optional[BufferControl]: + """ + Return the :class:`.BufferControl` in which we are searching or `None`. + """ + # Not every `UIControl` is a `BufferControl`. This only applies to + # `BufferControl`. + control = self.current_control + + if isinstance(control, SearchBufferControl): + return self.search_links.get(control) + else: + return None + + def get_focusable_windows(self) -> Iterable[Window]: + """ + Return all the :class:`.Window` objects which are focusable (in the + 'modal' area). + """ + for w in self.walk_through_modal_area(): + if isinstance(w, Window) and w.content.is_focusable(): + yield w + + def get_visible_focusable_windows(self) -> List[Window]: + """ + Return a list of :class:`.Window` objects that are focusable. + """ + # focusable windows are windows that are visible, but also part of the + # modal container. Make sure to keep the ordering. + visible_windows = self.visible_windows + return [w for w in self.get_focusable_windows() if w in visible_windows] + + @property + def current_buffer(self) -> Optional[Buffer]: + """ + The currently focused :class:`~.Buffer` or `None`. + """ + ui_control = self.current_control + if isinstance(ui_control, BufferControl): + return ui_control.buffer + return None + + def get_buffer_by_name(self, buffer_name: str) -> Optional[Buffer]: + """ + Look in the layout for a buffer with the given name. + Return `None` when nothing was found. + """ + for w in self.walk(): + if isinstance(w, Window) and isinstance(w.content, BufferControl): + if w.content.buffer.name == buffer_name: + return w.content.buffer + return None + + @property + def buffer_has_focus(self) -> bool: + """ + Return `True` if the currently focused control is a + :class:`.BufferControl`. (For instance, used to determine whether the + default key bindings should be active or not.) + """ + ui_control = self.current_control + return isinstance(ui_control, BufferControl) + + @property + def previous_control(self) -> UIControl: + """ + Get the :class:`.UIControl` to previously had the focus. + """ + try: + return self._stack[-2].content + except IndexError: + return self._stack[-1].content + + def focus_last(self) -> None: + """ + Give the focus to the last focused control. + """ + if len(self._stack) > 1: + self._stack = self._stack[:-1] + + def focus_next(self) -> None: + """ + Focus the next visible/focusable Window. + """ + windows = self.get_visible_focusable_windows() + + if len(windows) > 0: + try: + index = windows.index(self.current_window) + except ValueError: + index = 0 + else: + index = (index + 1) % len(windows) + + self.focus(windows[index]) + + def focus_previous(self) -> None: + """ + Focus the previous visible/focusable Window. + """ + windows = self.get_visible_focusable_windows() + + if len(windows) > 0: + try: + index = windows.index(self.current_window) + except ValueError: + index = 0 + else: + index = (index - 1) % len(windows) + + self.focus(windows[index]) + + def walk(self) -> Iterable[Container]: + """ + Walk through all the layout nodes (and their children) and yield them. + """ + for i in walk(self.container): + yield i + + def walk_through_modal_area(self) -> Iterable[Container]: + """ + Walk through all the containers which are in the current 'modal' part + of the layout. + """ + # Go up in the tree, and find the root. (it will be a part of the + # layout, if the focus is in a modal part.) + root: Container = self.current_window + while not root.is_modal() and root in self._child_to_parent: + root = self._child_to_parent[root] + + for container in walk(root): + yield container + + def update_parents_relations(self) -> None: + """ + Update child->parent relationships mapping. + """ + parents = {} + + def walk(e: Container) -> None: + for c in e.get_children(): + parents[c] = e + walk(c) + + walk(self.container) + + self._child_to_parent = parents + + def reset(self) -> None: + # Remove all search links when the UI starts. + # (Important, for instance when control-c is been pressed while + # searching. The prompt cancels, but next `run()` call the search + # links are still there.) + self.search_links.clear() + + self.container.reset() + + def get_parent(self, container: Container) -> Optional[Container]: + """ + Return the parent container for the given container, or ``None``, if it + wasn't found. + """ + try: + return self._child_to_parent[container] + except KeyError: + return None + + +class InvalidLayoutError(Exception): + pass + + +def walk(container: Container, skip_hidden: bool = False) -> Iterable[Container]: + """ + Walk through layout, starting at this container. + """ + # When `skip_hidden` is set, don't go into disabled ConditionalContainer containers. + if ( + skip_hidden + and isinstance(container, ConditionalContainer) + and not container.filter() + ): + return + + yield container + + for c in container.get_children(): + # yield from walk(c) + yield from walk(c, skip_hidden=skip_hidden) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/margins.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/margins.py index 26205232c0..7c46819c24 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/margins.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/margins.py @@ -1,305 +1,305 @@ -""" -Margin implementations for a :class:`~prompt_toolkit.layout.containers.Window`. -""" -from abc import ABCMeta, abstractmethod -from typing import TYPE_CHECKING, Callable, Optional - -from prompt_toolkit.filters import FilterOrBool, to_filter -from prompt_toolkit.formatted_text import ( - StyleAndTextTuples, - fragment_list_to_text, - to_formatted_text, -) -from prompt_toolkit.utils import get_cwidth - -from .controls import UIContent - -if TYPE_CHECKING: - from .containers import WindowRenderInfo - -__all__ = [ - "Margin", - "NumberedMargin", - "ScrollbarMargin", - "ConditionalMargin", - "PromptMargin", -] - - -class Margin(metaclass=ABCMeta): - """ - Base interface for a margin. - """ - - @abstractmethod - def get_width(self, get_ui_content: Callable[[], UIContent]) -> int: - """ - Return the width that this margin is going to consume. - - :param get_ui_content: Callable that asks the user control to create - a :class:`.UIContent` instance. This can be used for instance to - obtain the number of lines. - """ - return 0 - - @abstractmethod - def create_margin( - self, window_render_info: "WindowRenderInfo", width: int, height: int - ) -> StyleAndTextTuples: - """ - Creates a margin. - This should return a list of (style_str, text) tuples. - - :param window_render_info: - :class:`~prompt_toolkit.layout.containers.WindowRenderInfo` - instance, generated after rendering and copying the visible part of - the :class:`~prompt_toolkit.layout.controls.UIControl` into the - :class:`~prompt_toolkit.layout.containers.Window`. - :param width: The width that's available for this margin. (As reported - by :meth:`.get_width`.) - :param height: The height that's available for this margin. (The height - of the :class:`~prompt_toolkit.layout.containers.Window`.) - """ - return [] - - -class NumberedMargin(Margin): - """ - Margin that displays the line numbers. - - :param relative: Number relative to the cursor position. Similar to the Vi - 'relativenumber' option. - :param display_tildes: Display tildes after the end of the document, just - like Vi does. - """ - - def __init__( - self, relative: FilterOrBool = False, display_tildes: FilterOrBool = False - ) -> None: - - self.relative = to_filter(relative) - self.display_tildes = to_filter(display_tildes) - - def get_width(self, get_ui_content: Callable[[], UIContent]) -> int: - line_count = get_ui_content().line_count - return max(3, len("%s" % line_count) + 1) - - def create_margin( - self, window_render_info: "WindowRenderInfo", width: int, height: int - ) -> StyleAndTextTuples: - relative = self.relative() - - style = "class:line-number" - style_current = "class:line-number.current" - - # Get current line number. - current_lineno = window_render_info.ui_content.cursor_position.y - - # Construct margin. - result: StyleAndTextTuples = [] - last_lineno = None - - for y, lineno in enumerate(window_render_info.displayed_lines): - # Only display line number if this line is not a continuation of the previous line. - if lineno != last_lineno: - if lineno is None: - pass - elif lineno == current_lineno: - # Current line. - if relative: - # Left align current number in relative mode. - result.append((style_current, "%i" % (lineno + 1))) - else: - result.append( - (style_current, ("%i " % (lineno + 1)).rjust(width)) - ) - else: - # Other lines. - if relative: - lineno = abs(lineno - current_lineno) - 1 - - result.append((style, ("%i " % (lineno + 1)).rjust(width))) - - last_lineno = lineno - result.append(("", "\n")) - - # Fill with tildes. - if self.display_tildes(): - while y < window_render_info.window_height: - result.append(("class:tilde", "~\n")) - y += 1 - - return result - - -class ConditionalMargin(Margin): - """ - Wrapper around other :class:`.Margin` classes to show/hide them. - """ - - def __init__(self, margin: Margin, filter: FilterOrBool) -> None: - self.margin = margin - self.filter = to_filter(filter) - - def get_width(self, get_ui_content: Callable[[], UIContent]) -> int: - if self.filter(): - return self.margin.get_width(get_ui_content) - else: - return 0 - - def create_margin( - self, window_render_info: "WindowRenderInfo", width: int, height: int - ) -> StyleAndTextTuples: - if width and self.filter(): - return self.margin.create_margin(window_render_info, width, height) - else: - return [] - - -class ScrollbarMargin(Margin): - """ - Margin displaying a scrollbar. - - :param display_arrows: Display scroll up/down arrows. - """ - - def __init__( - self, - display_arrows: FilterOrBool = False, - up_arrow_symbol: str = "^", - down_arrow_symbol: str = "v", - ) -> None: - - self.display_arrows = to_filter(display_arrows) - self.up_arrow_symbol = up_arrow_symbol - self.down_arrow_symbol = down_arrow_symbol - - def get_width(self, get_ui_content: Callable[[], UIContent]) -> int: - return 1 - - def create_margin( - self, window_render_info: "WindowRenderInfo", width: int, height: int - ) -> StyleAndTextTuples: - content_height = window_render_info.content_height - window_height = window_render_info.window_height - display_arrows = self.display_arrows() - - if display_arrows: - window_height -= 2 - - try: - fraction_visible = len(window_render_info.displayed_lines) / float( - content_height - ) - fraction_above = window_render_info.vertical_scroll / float(content_height) - - scrollbar_height = int( - min(window_height, max(1, window_height * fraction_visible)) - ) - scrollbar_top = int(window_height * fraction_above) - except ZeroDivisionError: - return [] - else: - - def is_scroll_button(row: int) -> bool: - "True if we should display a button on this row." - return scrollbar_top <= row <= scrollbar_top + scrollbar_height - - # Up arrow. - result: StyleAndTextTuples = [] - if display_arrows: - result.extend( - [ - ("class:scrollbar.arrow", self.up_arrow_symbol), - ("class:scrollbar", "\n"), - ] - ) - - # Scrollbar body. - scrollbar_background = "class:scrollbar.background" - scrollbar_background_start = "class:scrollbar.background,scrollbar.start" - scrollbar_button = "class:scrollbar.button" - scrollbar_button_end = "class:scrollbar.button,scrollbar.end" - - for i in range(window_height): - if is_scroll_button(i): - if not is_scroll_button(i + 1): - # Give the last cell a different style, because we - # want to underline this. - result.append((scrollbar_button_end, " ")) - else: - result.append((scrollbar_button, " ")) - else: - if is_scroll_button(i + 1): - result.append((scrollbar_background_start, " ")) - else: - result.append((scrollbar_background, " ")) - result.append(("", "\n")) - - # Down arrow - if display_arrows: - result.append(("class:scrollbar.arrow", self.down_arrow_symbol)) - - return result - - -class PromptMargin(Margin): - """ - [Deprecated] - - Create margin that displays a prompt. - This can display one prompt at the first line, and a continuation prompt - (e.g, just dots) on all the following lines. - - This `PromptMargin` implementation has been largely superseded in favor of - the `get_line_prefix` attribute of `Window`. The reason is that a margin is - always a fixed width, while `get_line_prefix` can return a variable width - prefix in front of every line, making it more powerful, especially for line - continuations. - - :param get_prompt: Callable returns formatted text or a list of - `(style_str, type)` tuples to be shown as the prompt at the first line. - :param get_continuation: Callable that takes three inputs. The width (int), - line_number (int), and is_soft_wrap (bool). It should return formatted - text or a list of `(style_str, type)` tuples for the next lines of the - input. - """ - - def __init__( - self, - get_prompt: Callable[[], StyleAndTextTuples], - get_continuation: Optional[ - Callable[[int, int, bool], StyleAndTextTuples] - ] = None, - ) -> None: - - self.get_prompt = get_prompt - self.get_continuation = get_continuation - - def get_width(self, get_ui_content: Callable[[], UIContent]) -> int: - "Width to report to the `Window`." - # Take the width from the first line. - text = fragment_list_to_text(self.get_prompt()) - return get_cwidth(text) - - def create_margin( - self, window_render_info: "WindowRenderInfo", width: int, height: int - ) -> StyleAndTextTuples: - get_continuation = self.get_continuation - result: StyleAndTextTuples = [] - - # First line. - result.extend(to_formatted_text(self.get_prompt())) - - # Next lines. - if get_continuation: - last_y = None - - for y in window_render_info.displayed_lines[1:]: - result.append(("", "\n")) - result.extend( - to_formatted_text(get_continuation(width, y, y == last_y)) - ) - last_y = y - - return result +""" +Margin implementations for a :class:`~prompt_toolkit.layout.containers.Window`. +""" +from abc import ABCMeta, abstractmethod +from typing import TYPE_CHECKING, Callable, Optional + +from prompt_toolkit.filters import FilterOrBool, to_filter +from prompt_toolkit.formatted_text import ( + StyleAndTextTuples, + fragment_list_to_text, + to_formatted_text, +) +from prompt_toolkit.utils import get_cwidth + +from .controls import UIContent + +if TYPE_CHECKING: + from .containers import WindowRenderInfo + +__all__ = [ + "Margin", + "NumberedMargin", + "ScrollbarMargin", + "ConditionalMargin", + "PromptMargin", +] + + +class Margin(metaclass=ABCMeta): + """ + Base interface for a margin. + """ + + @abstractmethod + def get_width(self, get_ui_content: Callable[[], UIContent]) -> int: + """ + Return the width that this margin is going to consume. + + :param get_ui_content: Callable that asks the user control to create + a :class:`.UIContent` instance. This can be used for instance to + obtain the number of lines. + """ + return 0 + + @abstractmethod + def create_margin( + self, window_render_info: "WindowRenderInfo", width: int, height: int + ) -> StyleAndTextTuples: + """ + Creates a margin. + This should return a list of (style_str, text) tuples. + + :param window_render_info: + :class:`~prompt_toolkit.layout.containers.WindowRenderInfo` + instance, generated after rendering and copying the visible part of + the :class:`~prompt_toolkit.layout.controls.UIControl` into the + :class:`~prompt_toolkit.layout.containers.Window`. + :param width: The width that's available for this margin. (As reported + by :meth:`.get_width`.) + :param height: The height that's available for this margin. (The height + of the :class:`~prompt_toolkit.layout.containers.Window`.) + """ + return [] + + +class NumberedMargin(Margin): + """ + Margin that displays the line numbers. + + :param relative: Number relative to the cursor position. Similar to the Vi + 'relativenumber' option. + :param display_tildes: Display tildes after the end of the document, just + like Vi does. + """ + + def __init__( + self, relative: FilterOrBool = False, display_tildes: FilterOrBool = False + ) -> None: + + self.relative = to_filter(relative) + self.display_tildes = to_filter(display_tildes) + + def get_width(self, get_ui_content: Callable[[], UIContent]) -> int: + line_count = get_ui_content().line_count + return max(3, len("%s" % line_count) + 1) + + def create_margin( + self, window_render_info: "WindowRenderInfo", width: int, height: int + ) -> StyleAndTextTuples: + relative = self.relative() + + style = "class:line-number" + style_current = "class:line-number.current" + + # Get current line number. + current_lineno = window_render_info.ui_content.cursor_position.y + + # Construct margin. + result: StyleAndTextTuples = [] + last_lineno = None + + for y, lineno in enumerate(window_render_info.displayed_lines): + # Only display line number if this line is not a continuation of the previous line. + if lineno != last_lineno: + if lineno is None: + pass + elif lineno == current_lineno: + # Current line. + if relative: + # Left align current number in relative mode. + result.append((style_current, "%i" % (lineno + 1))) + else: + result.append( + (style_current, ("%i " % (lineno + 1)).rjust(width)) + ) + else: + # Other lines. + if relative: + lineno = abs(lineno - current_lineno) - 1 + + result.append((style, ("%i " % (lineno + 1)).rjust(width))) + + last_lineno = lineno + result.append(("", "\n")) + + # Fill with tildes. + if self.display_tildes(): + while y < window_render_info.window_height: + result.append(("class:tilde", "~\n")) + y += 1 + + return result + + +class ConditionalMargin(Margin): + """ + Wrapper around other :class:`.Margin` classes to show/hide them. + """ + + def __init__(self, margin: Margin, filter: FilterOrBool) -> None: + self.margin = margin + self.filter = to_filter(filter) + + def get_width(self, get_ui_content: Callable[[], UIContent]) -> int: + if self.filter(): + return self.margin.get_width(get_ui_content) + else: + return 0 + + def create_margin( + self, window_render_info: "WindowRenderInfo", width: int, height: int + ) -> StyleAndTextTuples: + if width and self.filter(): + return self.margin.create_margin(window_render_info, width, height) + else: + return [] + + +class ScrollbarMargin(Margin): + """ + Margin displaying a scrollbar. + + :param display_arrows: Display scroll up/down arrows. + """ + + def __init__( + self, + display_arrows: FilterOrBool = False, + up_arrow_symbol: str = "^", + down_arrow_symbol: str = "v", + ) -> None: + + self.display_arrows = to_filter(display_arrows) + self.up_arrow_symbol = up_arrow_symbol + self.down_arrow_symbol = down_arrow_symbol + + def get_width(self, get_ui_content: Callable[[], UIContent]) -> int: + return 1 + + def create_margin( + self, window_render_info: "WindowRenderInfo", width: int, height: int + ) -> StyleAndTextTuples: + content_height = window_render_info.content_height + window_height = window_render_info.window_height + display_arrows = self.display_arrows() + + if display_arrows: + window_height -= 2 + + try: + fraction_visible = len(window_render_info.displayed_lines) / float( + content_height + ) + fraction_above = window_render_info.vertical_scroll / float(content_height) + + scrollbar_height = int( + min(window_height, max(1, window_height * fraction_visible)) + ) + scrollbar_top = int(window_height * fraction_above) + except ZeroDivisionError: + return [] + else: + + def is_scroll_button(row: int) -> bool: + "True if we should display a button on this row." + return scrollbar_top <= row <= scrollbar_top + scrollbar_height + + # Up arrow. + result: StyleAndTextTuples = [] + if display_arrows: + result.extend( + [ + ("class:scrollbar.arrow", self.up_arrow_symbol), + ("class:scrollbar", "\n"), + ] + ) + + # Scrollbar body. + scrollbar_background = "class:scrollbar.background" + scrollbar_background_start = "class:scrollbar.background,scrollbar.start" + scrollbar_button = "class:scrollbar.button" + scrollbar_button_end = "class:scrollbar.button,scrollbar.end" + + for i in range(window_height): + if is_scroll_button(i): + if not is_scroll_button(i + 1): + # Give the last cell a different style, because we + # want to underline this. + result.append((scrollbar_button_end, " ")) + else: + result.append((scrollbar_button, " ")) + else: + if is_scroll_button(i + 1): + result.append((scrollbar_background_start, " ")) + else: + result.append((scrollbar_background, " ")) + result.append(("", "\n")) + + # Down arrow + if display_arrows: + result.append(("class:scrollbar.arrow", self.down_arrow_symbol)) + + return result + + +class PromptMargin(Margin): + """ + [Deprecated] + + Create margin that displays a prompt. + This can display one prompt at the first line, and a continuation prompt + (e.g, just dots) on all the following lines. + + This `PromptMargin` implementation has been largely superseded in favor of + the `get_line_prefix` attribute of `Window`. The reason is that a margin is + always a fixed width, while `get_line_prefix` can return a variable width + prefix in front of every line, making it more powerful, especially for line + continuations. + + :param get_prompt: Callable returns formatted text or a list of + `(style_str, type)` tuples to be shown as the prompt at the first line. + :param get_continuation: Callable that takes three inputs. The width (int), + line_number (int), and is_soft_wrap (bool). It should return formatted + text or a list of `(style_str, type)` tuples for the next lines of the + input. + """ + + def __init__( + self, + get_prompt: Callable[[], StyleAndTextTuples], + get_continuation: Optional[ + Callable[[int, int, bool], StyleAndTextTuples] + ] = None, + ) -> None: + + self.get_prompt = get_prompt + self.get_continuation = get_continuation + + def get_width(self, get_ui_content: Callable[[], UIContent]) -> int: + "Width to report to the `Window`." + # Take the width from the first line. + text = fragment_list_to_text(self.get_prompt()) + return get_cwidth(text) + + def create_margin( + self, window_render_info: "WindowRenderInfo", width: int, height: int + ) -> StyleAndTextTuples: + get_continuation = self.get_continuation + result: StyleAndTextTuples = [] + + # First line. + result.extend(to_formatted_text(self.get_prompt())) + + # Next lines. + if get_continuation: + last_y = None + + for y in window_render_info.displayed_lines[1:]: + result.append(("", "\n")) + result.extend( + to_formatted_text(get_continuation(width, y, y == last_y)) + ) + last_y = y + + return result diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/menus.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/menus.py index 8998f5ed1d..557450c000 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/menus.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/menus.py @@ -1,722 +1,722 @@ -import math -from itertools import zip_longest -from typing import ( - TYPE_CHECKING, - Callable, - Dict, - Iterable, - List, - Optional, - Tuple, - TypeVar, - Union, - cast, -) - -from prompt_toolkit.application.current import get_app -from prompt_toolkit.buffer import CompletionState -from prompt_toolkit.completion import Completion -from prompt_toolkit.data_structures import Point -from prompt_toolkit.filters import ( - Condition, - FilterOrBool, - has_completions, - is_done, - to_filter, -) -from prompt_toolkit.formatted_text import ( - StyleAndTextTuples, - fragment_list_width, - to_formatted_text, -) -from prompt_toolkit.key_binding.key_processor import KeyPressEvent -from prompt_toolkit.layout.utils import explode_text_fragments -from prompt_toolkit.mouse_events import MouseEvent, MouseEventType -from prompt_toolkit.utils import get_cwidth - -from .containers import ConditionalContainer, HSplit, ScrollOffsets, Window -from .controls import GetLinePrefixCallable, UIContent, UIControl -from .dimension import Dimension -from .margins import ScrollbarMargin - -if TYPE_CHECKING: - from prompt_toolkit.key_binding.key_bindings import ( - KeyBindings, - NotImplementedOrNone, - ) - - -__all__ = [ - "CompletionsMenu", - "MultiColumnCompletionsMenu", -] - -E = KeyPressEvent - - -class CompletionsMenuControl(UIControl): - """ - Helper for drawing the complete menu to the screen. - - :param scroll_offset: Number (integer) representing the preferred amount of - completions to be displayed before and after the current one. When this - is a very high number, the current completion will be shown in the - middle most of the time. - """ - - # Preferred minimum size of the menu control. - # The CompletionsMenu class defines a width of 8, and there is a scrollbar - # of 1.) - MIN_WIDTH = 7 - - def has_focus(self) -> bool: - return False - - def preferred_width(self, max_available_width: int) -> Optional[int]: - complete_state = get_app().current_buffer.complete_state - if complete_state: - menu_width = self._get_menu_width(500, complete_state) - menu_meta_width = self._get_menu_meta_width(500, complete_state) - - return menu_width + menu_meta_width - else: - return 0 - - def preferred_height( - self, - width: int, - max_available_height: int, - wrap_lines: bool, - get_line_prefix: Optional[GetLinePrefixCallable], - ) -> Optional[int]: - - complete_state = get_app().current_buffer.complete_state - if complete_state: - return len(complete_state.completions) - else: - return 0 - - def create_content(self, width: int, height: int) -> UIContent: - """ - Create a UIContent object for this control. - """ - complete_state = get_app().current_buffer.complete_state - if complete_state: - completions = complete_state.completions - index = complete_state.complete_index # Can be None! - - # Calculate width of completions menu. - menu_width = self._get_menu_width(width, complete_state) - menu_meta_width = self._get_menu_meta_width( - width - menu_width, complete_state - ) - show_meta = self._show_meta(complete_state) - - def get_line(i: int) -> StyleAndTextTuples: - c = completions[i] - is_current_completion = i == index - result = _get_menu_item_fragments( - c, is_current_completion, menu_width, space_after=True - ) - - if show_meta: - result += self._get_menu_item_meta_fragments( - c, is_current_completion, menu_meta_width - ) - return result - - return UIContent( - get_line=get_line, - cursor_position=Point(x=0, y=index or 0), - line_count=len(completions), - ) - - return UIContent() - - def _show_meta(self, complete_state: CompletionState) -> bool: - """ - Return ``True`` if we need to show a column with meta information. - """ - return any(c.display_meta_text for c in complete_state.completions) - - def _get_menu_width(self, max_width: int, complete_state: CompletionState) -> int: - """ - Return the width of the main column. - """ - return min( - max_width, - max( - self.MIN_WIDTH, - max(get_cwidth(c.display_text) for c in complete_state.completions) + 2, - ), - ) - - def _get_menu_meta_width( - self, max_width: int, complete_state: CompletionState - ) -> int: - """ - Return the width of the meta column. - """ - - def meta_width(completion: Completion) -> int: - return get_cwidth(completion.display_meta_text) - - if self._show_meta(complete_state): - return min( - max_width, max(meta_width(c) for c in complete_state.completions) + 2 - ) - else: - return 0 - - def _get_menu_item_meta_fragments( - self, completion: Completion, is_current_completion: bool, width: int - ) -> StyleAndTextTuples: - - if is_current_completion: - style_str = "class:completion-menu.meta.completion.current" - else: - style_str = "class:completion-menu.meta.completion" - - text, tw = _trim_formatted_text(completion.display_meta, width - 2) - padding = " " * (width - 1 - tw) - - return to_formatted_text( - cast(StyleAndTextTuples, []) + [("", " ")] + text + [("", padding)], - style=style_str, - ) - - def mouse_handler(self, mouse_event: MouseEvent) -> "NotImplementedOrNone": - """ - Handle mouse events: clicking and scrolling. - """ - b = get_app().current_buffer - - if mouse_event.event_type == MouseEventType.MOUSE_UP: - # Select completion. - b.go_to_completion(mouse_event.position.y) - b.complete_state = None - - elif mouse_event.event_type == MouseEventType.SCROLL_DOWN: - # Scroll up. - b.complete_next(count=3, disable_wrap_around=True) - - elif mouse_event.event_type == MouseEventType.SCROLL_UP: - # Scroll down. - b.complete_previous(count=3, disable_wrap_around=True) - - return None - - -def _get_menu_item_fragments( - completion: Completion, - is_current_completion: bool, - width: int, - space_after: bool = False, -) -> StyleAndTextTuples: - """ - Get the style/text tuples for a menu item, styled and trimmed to the given - width. - """ - if is_current_completion: - style_str = "class:completion-menu.completion.current %s %s" % ( - completion.style, - completion.selected_style, - ) - else: - style_str = "class:completion-menu.completion " + completion.style - - text, tw = _trim_formatted_text( - completion.display, (width - 2 if space_after else width - 1) - ) - - padding = " " * (width - 1 - tw) - - return to_formatted_text( - cast(StyleAndTextTuples, []) + [("", " ")] + text + [("", padding)], - style=style_str, - ) - - -def _trim_formatted_text( - formatted_text: StyleAndTextTuples, max_width: int -) -> Tuple[StyleAndTextTuples, int]: - """ - Trim the text to `max_width`, append dots when the text is too long. - Returns (text, width) tuple. - """ - width = fragment_list_width(formatted_text) - - # When the text is too wide, trim it. - if width > max_width: - result = [] # Text fragments. - remaining_width = max_width - 3 - - for style_and_ch in explode_text_fragments(formatted_text): - ch_width = get_cwidth(style_and_ch[1]) - - if ch_width <= remaining_width: - result.append(style_and_ch) - remaining_width -= ch_width - else: - break - - result.append(("", "...")) - - return result, max_width - remaining_width - else: - return formatted_text, width - - -class CompletionsMenu(ConditionalContainer): - # NOTE: We use a pretty big z_index by default. Menus are supposed to be - # above anything else. We also want to make sure that the content is - # visible at the point where we draw this menu. - def __init__( - self, - max_height: Optional[int] = None, - scroll_offset: Union[int, Callable[[], int]] = 0, - extra_filter: FilterOrBool = True, - display_arrows: FilterOrBool = False, +import math +from itertools import zip_longest +from typing import ( + TYPE_CHECKING, + Callable, + Dict, + Iterable, + List, + Optional, + Tuple, + TypeVar, + Union, + cast, +) + +from prompt_toolkit.application.current import get_app +from prompt_toolkit.buffer import CompletionState +from prompt_toolkit.completion import Completion +from prompt_toolkit.data_structures import Point +from prompt_toolkit.filters import ( + Condition, + FilterOrBool, + has_completions, + is_done, + to_filter, +) +from prompt_toolkit.formatted_text import ( + StyleAndTextTuples, + fragment_list_width, + to_formatted_text, +) +from prompt_toolkit.key_binding.key_processor import KeyPressEvent +from prompt_toolkit.layout.utils import explode_text_fragments +from prompt_toolkit.mouse_events import MouseEvent, MouseEventType +from prompt_toolkit.utils import get_cwidth + +from .containers import ConditionalContainer, HSplit, ScrollOffsets, Window +from .controls import GetLinePrefixCallable, UIContent, UIControl +from .dimension import Dimension +from .margins import ScrollbarMargin + +if TYPE_CHECKING: + from prompt_toolkit.key_binding.key_bindings import ( + KeyBindings, + NotImplementedOrNone, + ) + + +__all__ = [ + "CompletionsMenu", + "MultiColumnCompletionsMenu", +] + +E = KeyPressEvent + + +class CompletionsMenuControl(UIControl): + """ + Helper for drawing the complete menu to the screen. + + :param scroll_offset: Number (integer) representing the preferred amount of + completions to be displayed before and after the current one. When this + is a very high number, the current completion will be shown in the + middle most of the time. + """ + + # Preferred minimum size of the menu control. + # The CompletionsMenu class defines a width of 8, and there is a scrollbar + # of 1.) + MIN_WIDTH = 7 + + def has_focus(self) -> bool: + return False + + def preferred_width(self, max_available_width: int) -> Optional[int]: + complete_state = get_app().current_buffer.complete_state + if complete_state: + menu_width = self._get_menu_width(500, complete_state) + menu_meta_width = self._get_menu_meta_width(500, complete_state) + + return menu_width + menu_meta_width + else: + return 0 + + def preferred_height( + self, + width: int, + max_available_height: int, + wrap_lines: bool, + get_line_prefix: Optional[GetLinePrefixCallable], + ) -> Optional[int]: + + complete_state = get_app().current_buffer.complete_state + if complete_state: + return len(complete_state.completions) + else: + return 0 + + def create_content(self, width: int, height: int) -> UIContent: + """ + Create a UIContent object for this control. + """ + complete_state = get_app().current_buffer.complete_state + if complete_state: + completions = complete_state.completions + index = complete_state.complete_index # Can be None! + + # Calculate width of completions menu. + menu_width = self._get_menu_width(width, complete_state) + menu_meta_width = self._get_menu_meta_width( + width - menu_width, complete_state + ) + show_meta = self._show_meta(complete_state) + + def get_line(i: int) -> StyleAndTextTuples: + c = completions[i] + is_current_completion = i == index + result = _get_menu_item_fragments( + c, is_current_completion, menu_width, space_after=True + ) + + if show_meta: + result += self._get_menu_item_meta_fragments( + c, is_current_completion, menu_meta_width + ) + return result + + return UIContent( + get_line=get_line, + cursor_position=Point(x=0, y=index or 0), + line_count=len(completions), + ) + + return UIContent() + + def _show_meta(self, complete_state: CompletionState) -> bool: + """ + Return ``True`` if we need to show a column with meta information. + """ + return any(c.display_meta_text for c in complete_state.completions) + + def _get_menu_width(self, max_width: int, complete_state: CompletionState) -> int: + """ + Return the width of the main column. + """ + return min( + max_width, + max( + self.MIN_WIDTH, + max(get_cwidth(c.display_text) for c in complete_state.completions) + 2, + ), + ) + + def _get_menu_meta_width( + self, max_width: int, complete_state: CompletionState + ) -> int: + """ + Return the width of the meta column. + """ + + def meta_width(completion: Completion) -> int: + return get_cwidth(completion.display_meta_text) + + if self._show_meta(complete_state): + return min( + max_width, max(meta_width(c) for c in complete_state.completions) + 2 + ) + else: + return 0 + + def _get_menu_item_meta_fragments( + self, completion: Completion, is_current_completion: bool, width: int + ) -> StyleAndTextTuples: + + if is_current_completion: + style_str = "class:completion-menu.meta.completion.current" + else: + style_str = "class:completion-menu.meta.completion" + + text, tw = _trim_formatted_text(completion.display_meta, width - 2) + padding = " " * (width - 1 - tw) + + return to_formatted_text( + cast(StyleAndTextTuples, []) + [("", " ")] + text + [("", padding)], + style=style_str, + ) + + def mouse_handler(self, mouse_event: MouseEvent) -> "NotImplementedOrNone": + """ + Handle mouse events: clicking and scrolling. + """ + b = get_app().current_buffer + + if mouse_event.event_type == MouseEventType.MOUSE_UP: + # Select completion. + b.go_to_completion(mouse_event.position.y) + b.complete_state = None + + elif mouse_event.event_type == MouseEventType.SCROLL_DOWN: + # Scroll up. + b.complete_next(count=3, disable_wrap_around=True) + + elif mouse_event.event_type == MouseEventType.SCROLL_UP: + # Scroll down. + b.complete_previous(count=3, disable_wrap_around=True) + + return None + + +def _get_menu_item_fragments( + completion: Completion, + is_current_completion: bool, + width: int, + space_after: bool = False, +) -> StyleAndTextTuples: + """ + Get the style/text tuples for a menu item, styled and trimmed to the given + width. + """ + if is_current_completion: + style_str = "class:completion-menu.completion.current %s %s" % ( + completion.style, + completion.selected_style, + ) + else: + style_str = "class:completion-menu.completion " + completion.style + + text, tw = _trim_formatted_text( + completion.display, (width - 2 if space_after else width - 1) + ) + + padding = " " * (width - 1 - tw) + + return to_formatted_text( + cast(StyleAndTextTuples, []) + [("", " ")] + text + [("", padding)], + style=style_str, + ) + + +def _trim_formatted_text( + formatted_text: StyleAndTextTuples, max_width: int +) -> Tuple[StyleAndTextTuples, int]: + """ + Trim the text to `max_width`, append dots when the text is too long. + Returns (text, width) tuple. + """ + width = fragment_list_width(formatted_text) + + # When the text is too wide, trim it. + if width > max_width: + result = [] # Text fragments. + remaining_width = max_width - 3 + + for style_and_ch in explode_text_fragments(formatted_text): + ch_width = get_cwidth(style_and_ch[1]) + + if ch_width <= remaining_width: + result.append(style_and_ch) + remaining_width -= ch_width + else: + break + + result.append(("", "...")) + + return result, max_width - remaining_width + else: + return formatted_text, width + + +class CompletionsMenu(ConditionalContainer): + # NOTE: We use a pretty big z_index by default. Menus are supposed to be + # above anything else. We also want to make sure that the content is + # visible at the point where we draw this menu. + def __init__( + self, + max_height: Optional[int] = None, + scroll_offset: Union[int, Callable[[], int]] = 0, + extra_filter: FilterOrBool = True, + display_arrows: FilterOrBool = False, z_index: int = 10**8, - ) -> None: - - extra_filter = to_filter(extra_filter) - display_arrows = to_filter(display_arrows) - - super().__init__( - content=Window( - content=CompletionsMenuControl(), - width=Dimension(min=8), - height=Dimension(min=1, max=max_height), - scroll_offsets=ScrollOffsets(top=scroll_offset, bottom=scroll_offset), - right_margins=[ScrollbarMargin(display_arrows=display_arrows)], - dont_extend_width=True, - style="class:completion-menu", - z_index=z_index, - ), - # Show when there are completions but not at the point we are - # returning the input. - filter=has_completions & ~is_done & extra_filter, - ) - - -class MultiColumnCompletionMenuControl(UIControl): - """ - Completion menu that displays all the completions in several columns. - When there are more completions than space for them to be displayed, an - arrow is shown on the left or right side. - - `min_rows` indicates how many rows will be available in any possible case. - When this is larger than one, it will try to use less columns and more - rows until this value is reached. - Be careful passing in a too big value, if less than the given amount of - rows are available, more columns would have been required, but - `preferred_width` doesn't know about that and reports a too small value. - This results in less completions displayed and additional scrolling. - (It's a limitation of how the layout engine currently works: first the - widths are calculated, then the heights.) - - :param suggested_max_column_width: The suggested max width of a column. - The column can still be bigger than this, but if there is place for two - columns of this width, we will display two columns. This to avoid that - if there is one very wide completion, that it doesn't significantly - reduce the amount of columns. - """ - - _required_margin = 3 # One extra padding on the right + space for arrows. - - def __init__(self, min_rows: int = 3, suggested_max_column_width: int = 30) -> None: - assert min_rows >= 1 - - self.min_rows = min_rows - self.suggested_max_column_width = suggested_max_column_width - self.scroll = 0 - - # Info of last rendering. - self._rendered_rows = 0 - self._rendered_columns = 0 - self._total_columns = 0 - self._render_pos_to_completion: Dict[Tuple[int, int], Completion] = {} - self._render_left_arrow = False - self._render_right_arrow = False - self._render_width = 0 - - def reset(self) -> None: - self.scroll = 0 - - def has_focus(self) -> bool: - return False - - def preferred_width(self, max_available_width: int) -> Optional[int]: - """ - Preferred width: prefer to use at least min_rows, but otherwise as much - as possible horizontally. - """ - complete_state = get_app().current_buffer.complete_state - if complete_state is None: - return 0 - - column_width = self._get_column_width(complete_state) - result = int( - column_width - * math.ceil(len(complete_state.completions) / float(self.min_rows)) - ) - - # When the desired width is still more than the maximum available, - # reduce by removing columns until we are less than the available - # width. - while ( - result > column_width - and result > max_available_width - self._required_margin - ): - result -= column_width - return result + self._required_margin - - def preferred_height( - self, - width: int, - max_available_height: int, - wrap_lines: bool, - get_line_prefix: Optional[GetLinePrefixCallable], - ) -> Optional[int]: - """ - Preferred height: as much as needed in order to display all the completions. - """ - complete_state = get_app().current_buffer.complete_state - if complete_state is None: - return 0 - - column_width = self._get_column_width(complete_state) - column_count = max(1, (width - self._required_margin) // column_width) - - return int(math.ceil(len(complete_state.completions) / float(column_count))) - - def create_content(self, width: int, height: int) -> UIContent: - """ - Create a UIContent object for this menu. - """ - complete_state = get_app().current_buffer.complete_state - if complete_state is None: - return UIContent() - - column_width = self._get_column_width(complete_state) - self._render_pos_to_completion = {} - - _T = TypeVar("_T") - - def grouper( - n: int, iterable: Iterable[_T], fillvalue: Optional[_T] = None - ) -> Iterable[List[_T]]: - "grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx" - args = [iter(iterable)] * n - return zip_longest(fillvalue=fillvalue, *args) - - def is_current_completion(completion: Completion) -> bool: - "Returns True when this completion is the currently selected one." - return ( - complete_state is not None - and complete_state.complete_index is not None - and c == complete_state.current_completion - ) - - # Space required outside of the regular columns, for displaying the - # left and right arrow. - HORIZONTAL_MARGIN_REQUIRED = 3 - - # There should be at least one column, but it cannot be wider than - # the available width. - column_width = min(width - HORIZONTAL_MARGIN_REQUIRED, column_width) - - # However, when the columns tend to be very wide, because there are - # some very wide entries, shrink it anyway. - if column_width > self.suggested_max_column_width: - # `column_width` can still be bigger that `suggested_max_column_width`, - # but if there is place for two columns, we divide by two. - column_width //= column_width // self.suggested_max_column_width - - visible_columns = max(1, (width - self._required_margin) // column_width) - - columns_ = list(grouper(height, complete_state.completions)) - rows_ = list(zip(*columns_)) - - # Make sure the current completion is always visible: update scroll offset. - selected_column = (complete_state.complete_index or 0) // height - self.scroll = min( - selected_column, max(self.scroll, selected_column - visible_columns + 1) - ) - - render_left_arrow = self.scroll > 0 - render_right_arrow = self.scroll < len(rows_[0]) - visible_columns - - # Write completions to screen. - fragments_for_line = [] - - for row_index, row in enumerate(rows_): - fragments: StyleAndTextTuples = [] - middle_row = row_index == len(rows_) // 2 - - # Draw left arrow if we have hidden completions on the left. - if render_left_arrow: - fragments.append(("class:scrollbar", "<" if middle_row else " ")) - elif render_right_arrow: - # Reserve one column empty space. (If there is a right - # arrow right now, there can be a left arrow as well.) - fragments.append(("", " ")) - - # Draw row content. - for column_index, c in enumerate(row[self.scroll :][:visible_columns]): - if c is not None: - fragments += _get_menu_item_fragments( - c, is_current_completion(c), column_width, space_after=False - ) - - # Remember render position for mouse click handler. - for x in range(column_width): - self._render_pos_to_completion[ - (column_index * column_width + x, row_index) - ] = c - else: - fragments.append(("class:completion", " " * column_width)) - - # Draw trailing padding for this row. - # (_get_menu_item_fragments only returns padding on the left.) - if render_left_arrow or render_right_arrow: - fragments.append(("class:completion", " ")) - - # Draw right arrow if we have hidden completions on the right. - if render_right_arrow: - fragments.append(("class:scrollbar", ">" if middle_row else " ")) - elif render_left_arrow: - fragments.append(("class:completion", " ")) - - # Add line. - fragments_for_line.append( - to_formatted_text(fragments, style="class:completion-menu") - ) - - self._rendered_rows = height - self._rendered_columns = visible_columns - self._total_columns = len(columns_) - self._render_left_arrow = render_left_arrow - self._render_right_arrow = render_right_arrow - self._render_width = ( - column_width * visible_columns + render_left_arrow + render_right_arrow + 1 - ) - - def get_line(i: int) -> StyleAndTextTuples: - return fragments_for_line[i] - - return UIContent(get_line=get_line, line_count=len(rows_)) - - def _get_column_width(self, complete_state: CompletionState) -> int: - """ - Return the width of each column. - """ - return max(get_cwidth(c.display_text) for c in complete_state.completions) + 1 - - def mouse_handler(self, mouse_event: MouseEvent) -> "NotImplementedOrNone": - """ - Handle scroll and click events. - """ - b = get_app().current_buffer - - def scroll_left() -> None: - b.complete_previous(count=self._rendered_rows, disable_wrap_around=True) - self.scroll = max(0, self.scroll - 1) - - def scroll_right() -> None: - b.complete_next(count=self._rendered_rows, disable_wrap_around=True) - self.scroll = min( - self._total_columns - self._rendered_columns, self.scroll + 1 - ) - - if mouse_event.event_type == MouseEventType.SCROLL_DOWN: - scroll_right() - - elif mouse_event.event_type == MouseEventType.SCROLL_UP: - scroll_left() - - elif mouse_event.event_type == MouseEventType.MOUSE_UP: - x = mouse_event.position.x - y = mouse_event.position.y - - # Mouse click on left arrow. - if x == 0: - if self._render_left_arrow: - scroll_left() - - # Mouse click on right arrow. - elif x == self._render_width - 1: - if self._render_right_arrow: - scroll_right() - - # Mouse click on completion. - else: - completion = self._render_pos_to_completion.get((x, y)) - if completion: - b.apply_completion(completion) - - return None - - def get_key_bindings(self) -> "KeyBindings": - """ - Expose key bindings that handle the left/right arrow keys when the menu - is displayed. - """ - from prompt_toolkit.key_binding.key_bindings import KeyBindings - - kb = KeyBindings() - - @Condition - def filter() -> bool: - "Only handle key bindings if this menu is visible." - app = get_app() - complete_state = app.current_buffer.complete_state - - # There need to be completions, and one needs to be selected. - if complete_state is None or complete_state.complete_index is None: - return False - - # This menu needs to be visible. - return any(window.content == self for window in app.layout.visible_windows) - - def move(right: bool = False) -> None: - buff = get_app().current_buffer - complete_state = buff.complete_state - - if complete_state is not None and complete_state.complete_index is not None: - # Calculate new complete index. - new_index = complete_state.complete_index - if right: - new_index += self._rendered_rows - else: - new_index -= self._rendered_rows - - if 0 <= new_index < len(complete_state.completions): - buff.go_to_completion(new_index) - - # NOTE: the is_global is required because the completion menu will - # never be focussed. - - @kb.add("left", is_global=True, filter=filter) - def _left(event: E) -> None: - move() - - @kb.add("right", is_global=True, filter=filter) - def _right(event: E) -> None: - move(True) - - return kb - - -class MultiColumnCompletionsMenu(HSplit): - """ - Container that displays the completions in several columns. - When `show_meta` (a :class:`~prompt_toolkit.filters.Filter`) evaluates - to True, it shows the meta information at the bottom. - """ - - def __init__( - self, - min_rows: int = 3, - suggested_max_column_width: int = 30, - show_meta: FilterOrBool = True, - extra_filter: FilterOrBool = True, + ) -> None: + + extra_filter = to_filter(extra_filter) + display_arrows = to_filter(display_arrows) + + super().__init__( + content=Window( + content=CompletionsMenuControl(), + width=Dimension(min=8), + height=Dimension(min=1, max=max_height), + scroll_offsets=ScrollOffsets(top=scroll_offset, bottom=scroll_offset), + right_margins=[ScrollbarMargin(display_arrows=display_arrows)], + dont_extend_width=True, + style="class:completion-menu", + z_index=z_index, + ), + # Show when there are completions but not at the point we are + # returning the input. + filter=has_completions & ~is_done & extra_filter, + ) + + +class MultiColumnCompletionMenuControl(UIControl): + """ + Completion menu that displays all the completions in several columns. + When there are more completions than space for them to be displayed, an + arrow is shown on the left or right side. + + `min_rows` indicates how many rows will be available in any possible case. + When this is larger than one, it will try to use less columns and more + rows until this value is reached. + Be careful passing in a too big value, if less than the given amount of + rows are available, more columns would have been required, but + `preferred_width` doesn't know about that and reports a too small value. + This results in less completions displayed and additional scrolling. + (It's a limitation of how the layout engine currently works: first the + widths are calculated, then the heights.) + + :param suggested_max_column_width: The suggested max width of a column. + The column can still be bigger than this, but if there is place for two + columns of this width, we will display two columns. This to avoid that + if there is one very wide completion, that it doesn't significantly + reduce the amount of columns. + """ + + _required_margin = 3 # One extra padding on the right + space for arrows. + + def __init__(self, min_rows: int = 3, suggested_max_column_width: int = 30) -> None: + assert min_rows >= 1 + + self.min_rows = min_rows + self.suggested_max_column_width = suggested_max_column_width + self.scroll = 0 + + # Info of last rendering. + self._rendered_rows = 0 + self._rendered_columns = 0 + self._total_columns = 0 + self._render_pos_to_completion: Dict[Tuple[int, int], Completion] = {} + self._render_left_arrow = False + self._render_right_arrow = False + self._render_width = 0 + + def reset(self) -> None: + self.scroll = 0 + + def has_focus(self) -> bool: + return False + + def preferred_width(self, max_available_width: int) -> Optional[int]: + """ + Preferred width: prefer to use at least min_rows, but otherwise as much + as possible horizontally. + """ + complete_state = get_app().current_buffer.complete_state + if complete_state is None: + return 0 + + column_width = self._get_column_width(complete_state) + result = int( + column_width + * math.ceil(len(complete_state.completions) / float(self.min_rows)) + ) + + # When the desired width is still more than the maximum available, + # reduce by removing columns until we are less than the available + # width. + while ( + result > column_width + and result > max_available_width - self._required_margin + ): + result -= column_width + return result + self._required_margin + + def preferred_height( + self, + width: int, + max_available_height: int, + wrap_lines: bool, + get_line_prefix: Optional[GetLinePrefixCallable], + ) -> Optional[int]: + """ + Preferred height: as much as needed in order to display all the completions. + """ + complete_state = get_app().current_buffer.complete_state + if complete_state is None: + return 0 + + column_width = self._get_column_width(complete_state) + column_count = max(1, (width - self._required_margin) // column_width) + + return int(math.ceil(len(complete_state.completions) / float(column_count))) + + def create_content(self, width: int, height: int) -> UIContent: + """ + Create a UIContent object for this menu. + """ + complete_state = get_app().current_buffer.complete_state + if complete_state is None: + return UIContent() + + column_width = self._get_column_width(complete_state) + self._render_pos_to_completion = {} + + _T = TypeVar("_T") + + def grouper( + n: int, iterable: Iterable[_T], fillvalue: Optional[_T] = None + ) -> Iterable[List[_T]]: + "grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx" + args = [iter(iterable)] * n + return zip_longest(fillvalue=fillvalue, *args) + + def is_current_completion(completion: Completion) -> bool: + "Returns True when this completion is the currently selected one." + return ( + complete_state is not None + and complete_state.complete_index is not None + and c == complete_state.current_completion + ) + + # Space required outside of the regular columns, for displaying the + # left and right arrow. + HORIZONTAL_MARGIN_REQUIRED = 3 + + # There should be at least one column, but it cannot be wider than + # the available width. + column_width = min(width - HORIZONTAL_MARGIN_REQUIRED, column_width) + + # However, when the columns tend to be very wide, because there are + # some very wide entries, shrink it anyway. + if column_width > self.suggested_max_column_width: + # `column_width` can still be bigger that `suggested_max_column_width`, + # but if there is place for two columns, we divide by two. + column_width //= column_width // self.suggested_max_column_width + + visible_columns = max(1, (width - self._required_margin) // column_width) + + columns_ = list(grouper(height, complete_state.completions)) + rows_ = list(zip(*columns_)) + + # Make sure the current completion is always visible: update scroll offset. + selected_column = (complete_state.complete_index or 0) // height + self.scroll = min( + selected_column, max(self.scroll, selected_column - visible_columns + 1) + ) + + render_left_arrow = self.scroll > 0 + render_right_arrow = self.scroll < len(rows_[0]) - visible_columns + + # Write completions to screen. + fragments_for_line = [] + + for row_index, row in enumerate(rows_): + fragments: StyleAndTextTuples = [] + middle_row = row_index == len(rows_) // 2 + + # Draw left arrow if we have hidden completions on the left. + if render_left_arrow: + fragments.append(("class:scrollbar", "<" if middle_row else " ")) + elif render_right_arrow: + # Reserve one column empty space. (If there is a right + # arrow right now, there can be a left arrow as well.) + fragments.append(("", " ")) + + # Draw row content. + for column_index, c in enumerate(row[self.scroll :][:visible_columns]): + if c is not None: + fragments += _get_menu_item_fragments( + c, is_current_completion(c), column_width, space_after=False + ) + + # Remember render position for mouse click handler. + for x in range(column_width): + self._render_pos_to_completion[ + (column_index * column_width + x, row_index) + ] = c + else: + fragments.append(("class:completion", " " * column_width)) + + # Draw trailing padding for this row. + # (_get_menu_item_fragments only returns padding on the left.) + if render_left_arrow or render_right_arrow: + fragments.append(("class:completion", " ")) + + # Draw right arrow if we have hidden completions on the right. + if render_right_arrow: + fragments.append(("class:scrollbar", ">" if middle_row else " ")) + elif render_left_arrow: + fragments.append(("class:completion", " ")) + + # Add line. + fragments_for_line.append( + to_formatted_text(fragments, style="class:completion-menu") + ) + + self._rendered_rows = height + self._rendered_columns = visible_columns + self._total_columns = len(columns_) + self._render_left_arrow = render_left_arrow + self._render_right_arrow = render_right_arrow + self._render_width = ( + column_width * visible_columns + render_left_arrow + render_right_arrow + 1 + ) + + def get_line(i: int) -> StyleAndTextTuples: + return fragments_for_line[i] + + return UIContent(get_line=get_line, line_count=len(rows_)) + + def _get_column_width(self, complete_state: CompletionState) -> int: + """ + Return the width of each column. + """ + return max(get_cwidth(c.display_text) for c in complete_state.completions) + 1 + + def mouse_handler(self, mouse_event: MouseEvent) -> "NotImplementedOrNone": + """ + Handle scroll and click events. + """ + b = get_app().current_buffer + + def scroll_left() -> None: + b.complete_previous(count=self._rendered_rows, disable_wrap_around=True) + self.scroll = max(0, self.scroll - 1) + + def scroll_right() -> None: + b.complete_next(count=self._rendered_rows, disable_wrap_around=True) + self.scroll = min( + self._total_columns - self._rendered_columns, self.scroll + 1 + ) + + if mouse_event.event_type == MouseEventType.SCROLL_DOWN: + scroll_right() + + elif mouse_event.event_type == MouseEventType.SCROLL_UP: + scroll_left() + + elif mouse_event.event_type == MouseEventType.MOUSE_UP: + x = mouse_event.position.x + y = mouse_event.position.y + + # Mouse click on left arrow. + if x == 0: + if self._render_left_arrow: + scroll_left() + + # Mouse click on right arrow. + elif x == self._render_width - 1: + if self._render_right_arrow: + scroll_right() + + # Mouse click on completion. + else: + completion = self._render_pos_to_completion.get((x, y)) + if completion: + b.apply_completion(completion) + + return None + + def get_key_bindings(self) -> "KeyBindings": + """ + Expose key bindings that handle the left/right arrow keys when the menu + is displayed. + """ + from prompt_toolkit.key_binding.key_bindings import KeyBindings + + kb = KeyBindings() + + @Condition + def filter() -> bool: + "Only handle key bindings if this menu is visible." + app = get_app() + complete_state = app.current_buffer.complete_state + + # There need to be completions, and one needs to be selected. + if complete_state is None or complete_state.complete_index is None: + return False + + # This menu needs to be visible. + return any(window.content == self for window in app.layout.visible_windows) + + def move(right: bool = False) -> None: + buff = get_app().current_buffer + complete_state = buff.complete_state + + if complete_state is not None and complete_state.complete_index is not None: + # Calculate new complete index. + new_index = complete_state.complete_index + if right: + new_index += self._rendered_rows + else: + new_index -= self._rendered_rows + + if 0 <= new_index < len(complete_state.completions): + buff.go_to_completion(new_index) + + # NOTE: the is_global is required because the completion menu will + # never be focussed. + + @kb.add("left", is_global=True, filter=filter) + def _left(event: E) -> None: + move() + + @kb.add("right", is_global=True, filter=filter) + def _right(event: E) -> None: + move(True) + + return kb + + +class MultiColumnCompletionsMenu(HSplit): + """ + Container that displays the completions in several columns. + When `show_meta` (a :class:`~prompt_toolkit.filters.Filter`) evaluates + to True, it shows the meta information at the bottom. + """ + + def __init__( + self, + min_rows: int = 3, + suggested_max_column_width: int = 30, + show_meta: FilterOrBool = True, + extra_filter: FilterOrBool = True, z_index: int = 10**8, - ) -> None: - - show_meta = to_filter(show_meta) - extra_filter = to_filter(extra_filter) - - # Display filter: show when there are completions but not at the point - # we are returning the input. - full_filter = has_completions & ~is_done & extra_filter - - @Condition - def any_completion_has_meta() -> bool: - complete_state = get_app().current_buffer.complete_state - return complete_state is not None and any( - c.display_meta for c in complete_state.completions - ) - - # Create child windows. - # NOTE: We don't set style='class:completion-menu' to the - # `MultiColumnCompletionMenuControl`, because this is used in a - # Float that is made transparent, and the size of the control - # doesn't always correspond exactly with the size of the - # generated content. - completions_window = ConditionalContainer( - content=Window( - content=MultiColumnCompletionMenuControl( - min_rows=min_rows, - suggested_max_column_width=suggested_max_column_width, - ), - width=Dimension(min=8), - height=Dimension(min=1), - ), - filter=full_filter, - ) - - meta_window = ConditionalContainer( - content=Window(content=_SelectedCompletionMetaControl()), - filter=show_meta & full_filter & any_completion_has_meta, - ) - - # Initialise split. - super().__init__([completions_window, meta_window], z_index=z_index) - - -class _SelectedCompletionMetaControl(UIControl): - """ - Control that shows the meta information of the selected completion. - """ - - def preferred_width(self, max_available_width: int) -> Optional[int]: - """ - Report the width of the longest meta text as the preferred width of this control. - - It could be that we use less width, but this way, we're sure that the - layout doesn't change when we select another completion (E.g. that - completions are suddenly shown in more or fewer columns.) - """ - app = get_app() - if app.current_buffer.complete_state: - state = app.current_buffer.complete_state - return 2 + max(get_cwidth(c.display_meta_text) for c in state.completions) - else: - return 0 - - def preferred_height( - self, - width: int, - max_available_height: int, - wrap_lines: bool, - get_line_prefix: Optional[GetLinePrefixCallable], - ) -> Optional[int]: - return 1 - - def create_content(self, width: int, height: int) -> UIContent: - fragments = self._get_text_fragments() - - def get_line(i: int) -> StyleAndTextTuples: - return fragments - - return UIContent(get_line=get_line, line_count=1 if fragments else 0) - - def _get_text_fragments(self) -> StyleAndTextTuples: - style = "class:completion-menu.multi-column-meta" - state = get_app().current_buffer.complete_state - - if ( - state - and state.current_completion - and state.current_completion.display_meta_text - ): - return to_formatted_text( - cast(StyleAndTextTuples, [("", " ")]) - + state.current_completion.display_meta - + [("", " ")], - style=style, - ) - - return [] + ) -> None: + + show_meta = to_filter(show_meta) + extra_filter = to_filter(extra_filter) + + # Display filter: show when there are completions but not at the point + # we are returning the input. + full_filter = has_completions & ~is_done & extra_filter + + @Condition + def any_completion_has_meta() -> bool: + complete_state = get_app().current_buffer.complete_state + return complete_state is not None and any( + c.display_meta for c in complete_state.completions + ) + + # Create child windows. + # NOTE: We don't set style='class:completion-menu' to the + # `MultiColumnCompletionMenuControl`, because this is used in a + # Float that is made transparent, and the size of the control + # doesn't always correspond exactly with the size of the + # generated content. + completions_window = ConditionalContainer( + content=Window( + content=MultiColumnCompletionMenuControl( + min_rows=min_rows, + suggested_max_column_width=suggested_max_column_width, + ), + width=Dimension(min=8), + height=Dimension(min=1), + ), + filter=full_filter, + ) + + meta_window = ConditionalContainer( + content=Window(content=_SelectedCompletionMetaControl()), + filter=show_meta & full_filter & any_completion_has_meta, + ) + + # Initialise split. + super().__init__([completions_window, meta_window], z_index=z_index) + + +class _SelectedCompletionMetaControl(UIControl): + """ + Control that shows the meta information of the selected completion. + """ + + def preferred_width(self, max_available_width: int) -> Optional[int]: + """ + Report the width of the longest meta text as the preferred width of this control. + + It could be that we use less width, but this way, we're sure that the + layout doesn't change when we select another completion (E.g. that + completions are suddenly shown in more or fewer columns.) + """ + app = get_app() + if app.current_buffer.complete_state: + state = app.current_buffer.complete_state + return 2 + max(get_cwidth(c.display_meta_text) for c in state.completions) + else: + return 0 + + def preferred_height( + self, + width: int, + max_available_height: int, + wrap_lines: bool, + get_line_prefix: Optional[GetLinePrefixCallable], + ) -> Optional[int]: + return 1 + + def create_content(self, width: int, height: int) -> UIContent: + fragments = self._get_text_fragments() + + def get_line(i: int) -> StyleAndTextTuples: + return fragments + + return UIContent(get_line=get_line, line_count=1 if fragments else 0) + + def _get_text_fragments(self) -> StyleAndTextTuples: + style = "class:completion-menu.multi-column-meta" + state = get_app().current_buffer.complete_state + + if ( + state + and state.current_completion + and state.current_completion.display_meta_text + ): + return to_formatted_text( + cast(StyleAndTextTuples, [("", " ")]) + + state.current_completion.display_meta + + [("", " ")], + style=style, + ) + + return [] diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/mouse_handlers.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/mouse_handlers.py index d1dbeca6a9..256231793a 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/mouse_handlers.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/mouse_handlers.py @@ -1,54 +1,54 @@ -from collections import defaultdict -from typing import TYPE_CHECKING, Callable, DefaultDict - -from prompt_toolkit.mouse_events import MouseEvent - -if TYPE_CHECKING: - from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone - -__all__ = [ - "MouseHandler", - "MouseHandlers", -] - - -MouseHandler = Callable[[MouseEvent], "NotImplementedOrNone"] - - -class MouseHandlers: - """ - Two dimensional raster of callbacks for mouse events. - """ - - def __init__(self) -> None: - def dummy_callback(mouse_event: MouseEvent) -> "NotImplementedOrNone": - """ - :param mouse_event: `MouseEvent` instance. - """ - return NotImplemented - - # NOTE: Previously, the data structure was a dictionary mapping (x,y) - # to the handlers. This however would be more inefficient when copying - # over the mouse handlers of the visible region in the scrollable pane. - - # Map y (row) to x (column) to handlers. - self.mouse_handlers: DefaultDict[ - int, DefaultDict[int, MouseHandler] - ] = defaultdict(lambda: defaultdict(lambda: dummy_callback)) - - def set_mouse_handler_for_range( - self, - x_min: int, - x_max: int, - y_min: int, - y_max: int, - handler: Callable[[MouseEvent], "NotImplementedOrNone"], - ) -> None: - """ - Set mouse handler for a region. - """ - for y in range(y_min, y_max): - row = self.mouse_handlers[y] - - for x in range(x_min, x_max): - row[x] = handler +from collections import defaultdict +from typing import TYPE_CHECKING, Callable, DefaultDict + +from prompt_toolkit.mouse_events import MouseEvent + +if TYPE_CHECKING: + from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone + +__all__ = [ + "MouseHandler", + "MouseHandlers", +] + + +MouseHandler = Callable[[MouseEvent], "NotImplementedOrNone"] + + +class MouseHandlers: + """ + Two dimensional raster of callbacks for mouse events. + """ + + def __init__(self) -> None: + def dummy_callback(mouse_event: MouseEvent) -> "NotImplementedOrNone": + """ + :param mouse_event: `MouseEvent` instance. + """ + return NotImplemented + + # NOTE: Previously, the data structure was a dictionary mapping (x,y) + # to the handlers. This however would be more inefficient when copying + # over the mouse handlers of the visible region in the scrollable pane. + + # Map y (row) to x (column) to handlers. + self.mouse_handlers: DefaultDict[ + int, DefaultDict[int, MouseHandler] + ] = defaultdict(lambda: defaultdict(lambda: dummy_callback)) + + def set_mouse_handler_for_range( + self, + x_min: int, + x_max: int, + y_min: int, + y_max: int, + handler: Callable[[MouseEvent], "NotImplementedOrNone"], + ) -> None: + """ + Set mouse handler for a region. + """ + for y in range(y_min, y_max): + row = self.mouse_handlers[y] + + for x in range(x_min, x_max): + row[x] = handler diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/processors.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/processors.py index d69af5d0d4..571e952971 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/processors.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/processors.py @@ -1,1029 +1,1029 @@ -""" -Processors are little transformation blocks that transform the fragments list -from a buffer before the BufferControl will render it to the screen. - -They can insert fragments before or after, or highlight fragments by replacing the -fragment types. -""" -import re -from abc import ABCMeta, abstractmethod -from typing import ( - TYPE_CHECKING, - Callable, - Hashable, - List, - Optional, - Tuple, - Type, - Union, - cast, -) - -from prompt_toolkit.application.current import get_app -from prompt_toolkit.cache import SimpleCache -from prompt_toolkit.document import Document -from prompt_toolkit.filters import FilterOrBool, to_filter, vi_insert_multiple_mode -from prompt_toolkit.formatted_text import ( - AnyFormattedText, - StyleAndTextTuples, - to_formatted_text, -) -from prompt_toolkit.formatted_text.utils import fragment_list_len, fragment_list_to_text -from prompt_toolkit.search import SearchDirection -from prompt_toolkit.utils import to_int, to_str - -from .utils import explode_text_fragments - -if TYPE_CHECKING: - from .controls import BufferControl, UIContent - -__all__ = [ - "Processor", - "TransformationInput", - "Transformation", - "DummyProcessor", - "HighlightSearchProcessor", - "HighlightIncrementalSearchProcessor", - "HighlightSelectionProcessor", - "PasswordProcessor", - "HighlightMatchingBracketProcessor", - "DisplayMultipleCursors", - "BeforeInput", - "ShowArg", - "AfterInput", - "AppendAutoSuggestion", - "ConditionalProcessor", - "ShowLeadingWhiteSpaceProcessor", - "ShowTrailingWhiteSpaceProcessor", - "TabsProcessor", - "ReverseSearchProcessor", - "DynamicProcessor", - "merge_processors", -] - - -class Processor(metaclass=ABCMeta): - """ - Manipulate the fragments for a given line in a - :class:`~prompt_toolkit.layout.controls.BufferControl`. - """ - - @abstractmethod - def apply_transformation( - self, transformation_input: "TransformationInput" - ) -> "Transformation": - """ - Apply transformation. Returns a :class:`.Transformation` instance. - - :param transformation_input: :class:`.TransformationInput` object. - """ - return Transformation(transformation_input.fragments) - - -SourceToDisplay = Callable[[int], int] -DisplayToSource = Callable[[int], int] - - -class TransformationInput: - """ - :param buffer_control: :class:`.BufferControl` instance. - :param lineno: The number of the line to which we apply the processor. - :param source_to_display: A function that returns the position in the - `fragments` for any position in the source string. (This takes - previous processors into account.) - :param fragments: List of fragments that we can transform. (Received from the - previous processor.) - """ - - def __init__( - self, - buffer_control: "BufferControl", - document: Document, - lineno: int, - source_to_display: SourceToDisplay, - fragments: StyleAndTextTuples, - width: int, - height: int, - ) -> None: - - self.buffer_control = buffer_control - self.document = document - self.lineno = lineno - self.source_to_display = source_to_display - self.fragments = fragments - self.width = width - self.height = height - - def unpack( - self, - ) -> Tuple[ - "BufferControl", Document, int, SourceToDisplay, StyleAndTextTuples, int, int - ]: - return ( - self.buffer_control, - self.document, - self.lineno, - self.source_to_display, - self.fragments, - self.width, - self.height, - ) - - -class Transformation: - """ - Transformation result, as returned by :meth:`.Processor.apply_transformation`. - - Important: Always make sure that the length of `document.text` is equal to - the length of all the text in `fragments`! - - :param fragments: The transformed fragments. To be displayed, or to pass to - the next processor. - :param source_to_display: Cursor position transformation from original - string to transformed string. - :param display_to_source: Cursor position transformed from source string to - original string. - """ - - def __init__( - self, - fragments: StyleAndTextTuples, - source_to_display: Optional[SourceToDisplay] = None, - display_to_source: Optional[DisplayToSource] = None, - ) -> None: - - self.fragments = fragments - self.source_to_display = source_to_display or (lambda i: i) - self.display_to_source = display_to_source or (lambda i: i) - - -class DummyProcessor(Processor): - """ - A `Processor` that doesn't do anything. - """ - - def apply_transformation( - self, transformation_input: TransformationInput - ) -> Transformation: - return Transformation(transformation_input.fragments) - - -class HighlightSearchProcessor(Processor): - """ - Processor that highlights search matches in the document. - Note that this doesn't support multiline search matches yet. - - The style classes 'search' and 'search.current' will be applied to the - content. - """ - - _classname = "search" - _classname_current = "search.current" - - def _get_search_text(self, buffer_control: "BufferControl") -> str: - """ - The text we are searching for. - """ - return buffer_control.search_state.text - - def apply_transformation( - self, transformation_input: TransformationInput - ) -> Transformation: - - ( - buffer_control, - document, - lineno, - source_to_display, - fragments, - _, - _, - ) = transformation_input.unpack() - - search_text = self._get_search_text(buffer_control) - searchmatch_fragment = " class:%s " % (self._classname,) - searchmatch_current_fragment = " class:%s " % (self._classname_current,) - - if search_text and not get_app().is_done: - # For each search match, replace the style string. - line_text = fragment_list_to_text(fragments) - fragments = explode_text_fragments(fragments) - - if buffer_control.search_state.ignore_case(): - flags = re.IGNORECASE - else: - flags = re.RegexFlag(0) - - # Get cursor column. - cursor_column: Optional[int] - if document.cursor_position_row == lineno: - cursor_column = source_to_display(document.cursor_position_col) - else: - cursor_column = None - - for match in re.finditer(re.escape(search_text), line_text, flags=flags): - if cursor_column is not None: - on_cursor = match.start() <= cursor_column < match.end() - else: - on_cursor = False - - for i in range(match.start(), match.end()): - old_fragment, text, *_ = fragments[i] - if on_cursor: - fragments[i] = ( - old_fragment + searchmatch_current_fragment, - fragments[i][1], - ) - else: - fragments[i] = ( - old_fragment + searchmatch_fragment, - fragments[i][1], - ) - - return Transformation(fragments) - - -class HighlightIncrementalSearchProcessor(HighlightSearchProcessor): - """ - Highlight the search terms that are used for highlighting the incremental - search. The style class 'incsearch' will be applied to the content. - - Important: this requires the `preview_search=True` flag to be set for the - `BufferControl`. Otherwise, the cursor position won't be set to the search - match while searching, and nothing happens. - """ - - _classname = "incsearch" - _classname_current = "incsearch.current" - - def _get_search_text(self, buffer_control: "BufferControl") -> str: - """ - The text we are searching for. - """ - # When the search buffer has focus, take that text. - search_buffer = buffer_control.search_buffer - if search_buffer is not None and search_buffer.text: - return search_buffer.text - return "" - - -class HighlightSelectionProcessor(Processor): - """ - Processor that highlights the selection in the document. - """ - - def apply_transformation( - self, transformation_input: TransformationInput - ) -> Transformation: - ( - buffer_control, - document, - lineno, - source_to_display, - fragments, - _, - _, - ) = transformation_input.unpack() - - selected_fragment = " class:selected " - - # In case of selection, highlight all matches. - selection_at_line = document.selection_range_at_line(lineno) - - if selection_at_line: - from_, to = selection_at_line - from_ = source_to_display(from_) - to = source_to_display(to) - - fragments = explode_text_fragments(fragments) - - if from_ == 0 and to == 0 and len(fragments) == 0: - # When this is an empty line, insert a space in order to - # visualise the selection. - return Transformation([(selected_fragment, " ")]) - else: - for i in range(from_, to): - if i < len(fragments): - old_fragment, old_text, *_ = fragments[i] - fragments[i] = (old_fragment + selected_fragment, old_text) - elif i == len(fragments): - fragments.append((selected_fragment, " ")) - - return Transformation(fragments) - - -class PasswordProcessor(Processor): - """ - Processor that masks the input. (For passwords.) - - :param char: (string) Character to be used. "*" by default. - """ - - def __init__(self, char: str = "*") -> None: - self.char = char - - def apply_transformation(self, ti: TransformationInput) -> Transformation: - fragments: StyleAndTextTuples = cast( - StyleAndTextTuples, - [ - (style, self.char * len(text), *handler) - for style, text, *handler in ti.fragments - ], - ) - - return Transformation(fragments) - - -class HighlightMatchingBracketProcessor(Processor): - """ - When the cursor is on or right after a bracket, it highlights the matching - bracket. - - :param max_cursor_distance: Only highlight matching brackets when the - cursor is within this distance. (From inside a `Processor`, we can't - know which lines will be visible on the screen. But we also don't want - to scan the whole document for matching brackets on each key press, so - we limit to this value.) - """ - - _closing_braces = "])}>" - - def __init__( - self, chars: str = "[](){}<>", max_cursor_distance: int = 1000 - ) -> None: - self.chars = chars - self.max_cursor_distance = max_cursor_distance - - self._positions_cache: SimpleCache[ - Hashable, List[Tuple[int, int]] - ] = SimpleCache(maxsize=8) - - def _get_positions_to_highlight(self, document: Document) -> List[Tuple[int, int]]: - """ - Return a list of (row, col) tuples that need to be highlighted. - """ - pos: Optional[int] - - # Try for the character under the cursor. - if document.current_char and document.current_char in self.chars: - pos = document.find_matching_bracket_position( - start_pos=document.cursor_position - self.max_cursor_distance, - end_pos=document.cursor_position + self.max_cursor_distance, - ) - - # Try for the character before the cursor. - elif ( - document.char_before_cursor - and document.char_before_cursor in self._closing_braces - and document.char_before_cursor in self.chars - ): - document = Document(document.text, document.cursor_position - 1) - - pos = document.find_matching_bracket_position( - start_pos=document.cursor_position - self.max_cursor_distance, - end_pos=document.cursor_position + self.max_cursor_distance, - ) - else: - pos = None - - # Return a list of (row, col) tuples that need to be highlighted. - if pos: - pos += document.cursor_position # pos is relative. - row, col = document.translate_index_to_position(pos) - return [ - (row, col), - (document.cursor_position_row, document.cursor_position_col), - ] - else: - return [] - - def apply_transformation( - self, transformation_input: TransformationInput - ) -> Transformation: - - ( - buffer_control, - document, - lineno, - source_to_display, - fragments, - _, - _, - ) = transformation_input.unpack() - - # When the application is in the 'done' state, don't highlight. - if get_app().is_done: - return Transformation(fragments) - - # Get the highlight positions. - key = (get_app().render_counter, document.text, document.cursor_position) - positions = self._positions_cache.get( - key, lambda: self._get_positions_to_highlight(document) - ) - - # Apply if positions were found at this line. - if positions: - for row, col in positions: - if row == lineno: - col = source_to_display(col) - fragments = explode_text_fragments(fragments) - style, text, *_ = fragments[col] - - if col == document.cursor_position_col: - style += " class:matching-bracket.cursor " - else: - style += " class:matching-bracket.other " - - fragments[col] = (style, text) - - return Transformation(fragments) - - -class DisplayMultipleCursors(Processor): - """ - When we're in Vi block insert mode, display all the cursors. - """ - - def apply_transformation( - self, transformation_input: TransformationInput - ) -> Transformation: - - ( - buffer_control, - document, - lineno, - source_to_display, - fragments, - _, - _, - ) = transformation_input.unpack() - - buff = buffer_control.buffer - - if vi_insert_multiple_mode(): - cursor_positions = buff.multiple_cursor_positions - fragments = explode_text_fragments(fragments) - - # If any cursor appears on the current line, highlight that. - start_pos = document.translate_row_col_to_index(lineno, 0) - end_pos = start_pos + len(document.lines[lineno]) - - fragment_suffix = " class:multiple-cursors" - - for p in cursor_positions: - if start_pos <= p <= end_pos: - column = source_to_display(p - start_pos) - - # Replace fragment. - try: - style, text, *_ = fragments[column] - except IndexError: - # Cursor needs to be displayed after the current text. - fragments.append((fragment_suffix, " ")) - else: - style += fragment_suffix - fragments[column] = (style, text) - - return Transformation(fragments) - else: - return Transformation(fragments) - - -class BeforeInput(Processor): - """ - Insert text before the input. - - :param text: This can be either plain text or formatted text - (or a callable that returns any of those). - :param style: style to be applied to this prompt/prefix. - """ - - def __init__(self, text: AnyFormattedText, style: str = "") -> None: - self.text = text - self.style = style - - def apply_transformation(self, ti: TransformationInput) -> Transformation: - source_to_display: Optional[SourceToDisplay] - display_to_source: Optional[DisplayToSource] - - if ti.lineno == 0: - # Get fragments. - fragments_before = to_formatted_text(self.text, self.style) - fragments = fragments_before + ti.fragments - - shift_position = fragment_list_len(fragments_before) - source_to_display = lambda i: i + shift_position - display_to_source = lambda i: i - shift_position - else: - fragments = ti.fragments - source_to_display = None - display_to_source = None - - return Transformation( - fragments, - source_to_display=source_to_display, - display_to_source=display_to_source, - ) - - def __repr__(self) -> str: - return "BeforeInput(%r, %r)" % (self.text, self.style) - - -class ShowArg(BeforeInput): - """ - Display the 'arg' in front of the input. - - This was used by the `PromptSession`, but now it uses the - `Window.get_line_prefix` function instead. - """ - - def __init__(self) -> None: - super().__init__(self._get_text_fragments) - - def _get_text_fragments(self) -> StyleAndTextTuples: - app = get_app() - if app.key_processor.arg is None: - return [] - else: - arg = app.key_processor.arg - - return [ - ("class:prompt.arg", "(arg: "), - ("class:prompt.arg.text", str(arg)), - ("class:prompt.arg", ") "), - ] - - def __repr__(self) -> str: - return "ShowArg()" - - -class AfterInput(Processor): - """ - Insert text after the input. - - :param text: This can be either plain text or formatted text - (or a callable that returns any of those). - :param style: style to be applied to this prompt/prefix. - """ - - def __init__(self, text: AnyFormattedText, style: str = "") -> None: - self.text = text - self.style = style - - def apply_transformation(self, ti: TransformationInput) -> Transformation: - # Insert fragments after the last line. - if ti.lineno == ti.document.line_count - 1: - # Get fragments. - fragments_after = to_formatted_text(self.text, self.style) - return Transformation(fragments=ti.fragments + fragments_after) - else: - return Transformation(fragments=ti.fragments) - - def __repr__(self) -> str: - return "%s(%r, style=%r)" % (self.__class__.__name__, self.text, self.style) - - -class AppendAutoSuggestion(Processor): - """ - Append the auto suggestion to the input. - (The user can then press the right arrow the insert the suggestion.) - """ - - def __init__(self, style: str = "class:auto-suggestion") -> None: - self.style = style - - def apply_transformation(self, ti: TransformationInput) -> Transformation: - # Insert fragments after the last line. - if ti.lineno == ti.document.line_count - 1: - buffer = ti.buffer_control.buffer - - if buffer.suggestion and ti.document.is_cursor_at_the_end: - suggestion = buffer.suggestion.text - else: - suggestion = "" - - return Transformation(fragments=ti.fragments + [(self.style, suggestion)]) - else: - return Transformation(fragments=ti.fragments) - - -class ShowLeadingWhiteSpaceProcessor(Processor): - """ - Make leading whitespace visible. - - :param get_char: Callable that returns one character. - """ - - def __init__( - self, - get_char: Optional[Callable[[], str]] = None, - style: str = "class:leading-whitespace", - ) -> None: - def default_get_char() -> str: - if "\xb7".encode(get_app().output.encoding(), "replace") == b"?": - return "." - else: - return "\xb7" - - self.style = style - self.get_char = get_char or default_get_char - - def apply_transformation(self, ti: TransformationInput) -> Transformation: - fragments = ti.fragments - - # Walk through all te fragments. - if fragments and fragment_list_to_text(fragments).startswith(" "): - t = (self.style, self.get_char()) - fragments = explode_text_fragments(fragments) - - for i in range(len(fragments)): - if fragments[i][1] == " ": - fragments[i] = t - else: - break - - return Transformation(fragments) - - -class ShowTrailingWhiteSpaceProcessor(Processor): - """ - Make trailing whitespace visible. - - :param get_char: Callable that returns one character. - """ - - def __init__( - self, - get_char: Optional[Callable[[], str]] = None, - style: str = "class:training-whitespace", - ) -> None: - def default_get_char() -> str: - if "\xb7".encode(get_app().output.encoding(), "replace") == b"?": - return "." - else: - return "\xb7" - - self.style = style - self.get_char = get_char or default_get_char - - def apply_transformation(self, ti: TransformationInput) -> Transformation: - fragments = ti.fragments - - if fragments and fragments[-1][1].endswith(" "): - t = (self.style, self.get_char()) - fragments = explode_text_fragments(fragments) - - # Walk backwards through all te fragments and replace whitespace. - for i in range(len(fragments) - 1, -1, -1): - char = fragments[i][1] - if char == " ": - fragments[i] = t - else: - break - - return Transformation(fragments) - - -class TabsProcessor(Processor): - """ - Render tabs as spaces (instead of ^I) or make them visible (for instance, - by replacing them with dots.) - - :param tabstop: Horizontal space taken by a tab. (`int` or callable that - returns an `int`). - :param char1: Character or callable that returns a character (text of - length one). This one is used for the first space taken by the tab. - :param char2: Like `char1`, but for the rest of the space. - """ - - def __init__( - self, - tabstop: Union[int, Callable[[], int]] = 4, - char1: Union[str, Callable[[], str]] = "|", - char2: Union[str, Callable[[], str]] = "\u2508", - style: str = "class:tab", - ) -> None: - - self.char1 = char1 - self.char2 = char2 - self.tabstop = tabstop - self.style = style - - def apply_transformation(self, ti: TransformationInput) -> Transformation: - tabstop = to_int(self.tabstop) - style = self.style - - # Create separator for tabs. - separator1 = to_str(self.char1) - separator2 = to_str(self.char2) - - # Transform fragments. - fragments = explode_text_fragments(ti.fragments) - - position_mappings = {} - result_fragments: StyleAndTextTuples = [] - pos = 0 - - for i, fragment_and_text in enumerate(fragments): - position_mappings[i] = pos - - if fragment_and_text[1] == "\t": - # Calculate how many characters we have to insert. - count = tabstop - (pos % tabstop) - if count == 0: - count = tabstop - - # Insert tab. - result_fragments.append((style, separator1)) - result_fragments.append((style, separator2 * (count - 1))) - pos += count - else: - result_fragments.append(fragment_and_text) - pos += 1 - - position_mappings[len(fragments)] = pos - # Add `pos+1` to mapping, because the cursor can be right after the - # line as well. - position_mappings[len(fragments) + 1] = pos + 1 - - def source_to_display(from_position: int) -> int: - "Maps original cursor position to the new one." - return position_mappings[from_position] - - def display_to_source(display_pos: int) -> int: - "Maps display cursor position to the original one." - position_mappings_reversed = {v: k for k, v in position_mappings.items()} - - while display_pos >= 0: - try: - return position_mappings_reversed[display_pos] - except KeyError: - display_pos -= 1 - return 0 - - return Transformation( - result_fragments, - source_to_display=source_to_display, - display_to_source=display_to_source, - ) - - -class ReverseSearchProcessor(Processor): - """ - Process to display the "(reverse-i-search)`...`:..." stuff around - the search buffer. - - Note: This processor is meant to be applied to the BufferControl that - contains the search buffer, it's not meant for the original input. - """ - - _excluded_input_processors: List[Type[Processor]] = [ - HighlightSearchProcessor, - HighlightSelectionProcessor, - BeforeInput, - AfterInput, - ] - - def _get_main_buffer( - self, buffer_control: "BufferControl" - ) -> Optional["BufferControl"]: - from prompt_toolkit.layout.controls import BufferControl - - prev_control = get_app().layout.search_target_buffer_control - if ( - isinstance(prev_control, BufferControl) - and prev_control.search_buffer_control == buffer_control - ): - return prev_control - return None - - def _content( - self, main_control: "BufferControl", ti: TransformationInput - ) -> "UIContent": - from prompt_toolkit.layout.controls import BufferControl - - # Emulate the BufferControl through which we are searching. - # For this we filter out some of the input processors. - excluded_processors = tuple(self._excluded_input_processors) - - def filter_processor(item: Processor) -> Optional[Processor]: - """Filter processors from the main control that we want to disable - here. This returns either an accepted processor or None.""" - # For a `_MergedProcessor`, check each individual processor, recursively. - if isinstance(item, _MergedProcessor): - accepted_processors = [filter_processor(p) for p in item.processors] - return merge_processors( - [p for p in accepted_processors if p is not None] - ) - - # For a `ConditionalProcessor`, check the body. - elif isinstance(item, ConditionalProcessor): - p = filter_processor(item.processor) - if p: - return ConditionalProcessor(p, item.filter) - - # Otherwise, check the processor itself. - else: - if not isinstance(item, excluded_processors): - return item - - return None - - filtered_processor = filter_processor( - merge_processors(main_control.input_processors or []) - ) - highlight_processor = HighlightIncrementalSearchProcessor() - - if filtered_processor: - new_processors = [filtered_processor, highlight_processor] - else: - new_processors = [highlight_processor] - - from .controls import SearchBufferControl - - assert isinstance(ti.buffer_control, SearchBufferControl) - - buffer_control = BufferControl( - buffer=main_control.buffer, - input_processors=new_processors, - include_default_input_processors=False, - lexer=main_control.lexer, - preview_search=True, - search_buffer_control=ti.buffer_control, - ) - - return buffer_control.create_content(ti.width, ti.height, preview_search=True) - - def apply_transformation(self, ti: TransformationInput) -> Transformation: - from .controls import SearchBufferControl - - assert isinstance( - ti.buffer_control, SearchBufferControl - ), "`ReverseSearchProcessor` should be applied to a `SearchBufferControl` only." - - source_to_display: Optional[SourceToDisplay] - display_to_source: Optional[DisplayToSource] - - main_control = self._get_main_buffer(ti.buffer_control) - - if ti.lineno == 0 and main_control: - content = self._content(main_control, ti) - - # Get the line from the original document for this search. - line_fragments = content.get_line(content.cursor_position.y) - - if main_control.search_state.direction == SearchDirection.FORWARD: - direction_text = "i-search" - else: - direction_text = "reverse-i-search" - - fragments_before: StyleAndTextTuples = [ - ("class:prompt.search", "("), - ("class:prompt.search", direction_text), - ("class:prompt.search", ")`"), - ] - - fragments = ( - fragments_before - + [ - ("class:prompt.search.text", fragment_list_to_text(ti.fragments)), - ("", "': "), - ] - + line_fragments - ) - - shift_position = fragment_list_len(fragments_before) - source_to_display = lambda i: i + shift_position - display_to_source = lambda i: i - shift_position - else: - source_to_display = None - display_to_source = None - fragments = ti.fragments - - return Transformation( - fragments, - source_to_display=source_to_display, - display_to_source=display_to_source, - ) - - -class ConditionalProcessor(Processor): - """ - Processor that applies another processor, according to a certain condition. - Example:: - - # Create a function that returns whether or not the processor should - # currently be applied. - def highlight_enabled(): - return true_or_false - - # Wrapped it in a `ConditionalProcessor` for usage in a `BufferControl`. - BufferControl(input_processors=[ - ConditionalProcessor(HighlightSearchProcessor(), - Condition(highlight_enabled))]) - - :param processor: :class:`.Processor` instance. - :param filter: :class:`~prompt_toolkit.filters.Filter` instance. - """ - - def __init__(self, processor: Processor, filter: FilterOrBool) -> None: - self.processor = processor - self.filter = to_filter(filter) - - def apply_transformation( - self, transformation_input: TransformationInput - ) -> Transformation: - # Run processor when enabled. - if self.filter(): - return self.processor.apply_transformation(transformation_input) - else: - return Transformation(transformation_input.fragments) - - def __repr__(self) -> str: - return "%s(processor=%r, filter=%r)" % ( - self.__class__.__name__, - self.processor, - self.filter, - ) - - -class DynamicProcessor(Processor): - """ - Processor class that dynamically returns any Processor. - - :param get_processor: Callable that returns a :class:`.Processor` instance. - """ - - def __init__(self, get_processor: Callable[[], Optional[Processor]]) -> None: - self.get_processor = get_processor - - def apply_transformation(self, ti: TransformationInput) -> Transformation: - processor = self.get_processor() or DummyProcessor() - return processor.apply_transformation(ti) - - -def merge_processors(processors: List[Processor]) -> Processor: - """ - Merge multiple `Processor` objects into one. - """ - if len(processors) == 0: - return DummyProcessor() - - if len(processors) == 1: - return processors[0] # Nothing to merge. - - return _MergedProcessor(processors) - - -class _MergedProcessor(Processor): - """ - Processor that groups multiple other `Processor` objects, but exposes an - API as if it is one `Processor`. - """ - - def __init__(self, processors: List[Processor]): - self.processors = processors - - def apply_transformation(self, ti: TransformationInput) -> Transformation: - source_to_display_functions = [ti.source_to_display] - display_to_source_functions = [] - fragments = ti.fragments - - def source_to_display(i: int) -> int: - """Translate x position from the buffer to the x position in the - processor fragments list.""" - for f in source_to_display_functions: - i = f(i) - return i - - for p in self.processors: - transformation = p.apply_transformation( - TransformationInput( - ti.buffer_control, - ti.document, - ti.lineno, - source_to_display, - fragments, - ti.width, - ti.height, - ) - ) - fragments = transformation.fragments - display_to_source_functions.append(transformation.display_to_source) - source_to_display_functions.append(transformation.source_to_display) - - def display_to_source(i: int) -> int: - for f in reversed(display_to_source_functions): - i = f(i) - return i - - # In the case of a nested _MergedProcessor, each processor wants to - # receive a 'source_to_display' function (as part of the - # TransformationInput) that has everything in the chain before - # included, because it can be called as part of the - # `apply_transformation` function. However, this first - # `source_to_display` should not be part of the output that we are - # returning. (This is the most consistent with `display_to_source`.) - del source_to_display_functions[:1] - - return Transformation(fragments, source_to_display, display_to_source) +""" +Processors are little transformation blocks that transform the fragments list +from a buffer before the BufferControl will render it to the screen. + +They can insert fragments before or after, or highlight fragments by replacing the +fragment types. +""" +import re +from abc import ABCMeta, abstractmethod +from typing import ( + TYPE_CHECKING, + Callable, + Hashable, + List, + Optional, + Tuple, + Type, + Union, + cast, +) + +from prompt_toolkit.application.current import get_app +from prompt_toolkit.cache import SimpleCache +from prompt_toolkit.document import Document +from prompt_toolkit.filters import FilterOrBool, to_filter, vi_insert_multiple_mode +from prompt_toolkit.formatted_text import ( + AnyFormattedText, + StyleAndTextTuples, + to_formatted_text, +) +from prompt_toolkit.formatted_text.utils import fragment_list_len, fragment_list_to_text +from prompt_toolkit.search import SearchDirection +from prompt_toolkit.utils import to_int, to_str + +from .utils import explode_text_fragments + +if TYPE_CHECKING: + from .controls import BufferControl, UIContent + +__all__ = [ + "Processor", + "TransformationInput", + "Transformation", + "DummyProcessor", + "HighlightSearchProcessor", + "HighlightIncrementalSearchProcessor", + "HighlightSelectionProcessor", + "PasswordProcessor", + "HighlightMatchingBracketProcessor", + "DisplayMultipleCursors", + "BeforeInput", + "ShowArg", + "AfterInput", + "AppendAutoSuggestion", + "ConditionalProcessor", + "ShowLeadingWhiteSpaceProcessor", + "ShowTrailingWhiteSpaceProcessor", + "TabsProcessor", + "ReverseSearchProcessor", + "DynamicProcessor", + "merge_processors", +] + + +class Processor(metaclass=ABCMeta): + """ + Manipulate the fragments for a given line in a + :class:`~prompt_toolkit.layout.controls.BufferControl`. + """ + + @abstractmethod + def apply_transformation( + self, transformation_input: "TransformationInput" + ) -> "Transformation": + """ + Apply transformation. Returns a :class:`.Transformation` instance. + + :param transformation_input: :class:`.TransformationInput` object. + """ + return Transformation(transformation_input.fragments) + + +SourceToDisplay = Callable[[int], int] +DisplayToSource = Callable[[int], int] + + +class TransformationInput: + """ + :param buffer_control: :class:`.BufferControl` instance. + :param lineno: The number of the line to which we apply the processor. + :param source_to_display: A function that returns the position in the + `fragments` for any position in the source string. (This takes + previous processors into account.) + :param fragments: List of fragments that we can transform. (Received from the + previous processor.) + """ + + def __init__( + self, + buffer_control: "BufferControl", + document: Document, + lineno: int, + source_to_display: SourceToDisplay, + fragments: StyleAndTextTuples, + width: int, + height: int, + ) -> None: + + self.buffer_control = buffer_control + self.document = document + self.lineno = lineno + self.source_to_display = source_to_display + self.fragments = fragments + self.width = width + self.height = height + + def unpack( + self, + ) -> Tuple[ + "BufferControl", Document, int, SourceToDisplay, StyleAndTextTuples, int, int + ]: + return ( + self.buffer_control, + self.document, + self.lineno, + self.source_to_display, + self.fragments, + self.width, + self.height, + ) + + +class Transformation: + """ + Transformation result, as returned by :meth:`.Processor.apply_transformation`. + + Important: Always make sure that the length of `document.text` is equal to + the length of all the text in `fragments`! + + :param fragments: The transformed fragments. To be displayed, or to pass to + the next processor. + :param source_to_display: Cursor position transformation from original + string to transformed string. + :param display_to_source: Cursor position transformed from source string to + original string. + """ + + def __init__( + self, + fragments: StyleAndTextTuples, + source_to_display: Optional[SourceToDisplay] = None, + display_to_source: Optional[DisplayToSource] = None, + ) -> None: + + self.fragments = fragments + self.source_to_display = source_to_display or (lambda i: i) + self.display_to_source = display_to_source or (lambda i: i) + + +class DummyProcessor(Processor): + """ + A `Processor` that doesn't do anything. + """ + + def apply_transformation( + self, transformation_input: TransformationInput + ) -> Transformation: + return Transformation(transformation_input.fragments) + + +class HighlightSearchProcessor(Processor): + """ + Processor that highlights search matches in the document. + Note that this doesn't support multiline search matches yet. + + The style classes 'search' and 'search.current' will be applied to the + content. + """ + + _classname = "search" + _classname_current = "search.current" + + def _get_search_text(self, buffer_control: "BufferControl") -> str: + """ + The text we are searching for. + """ + return buffer_control.search_state.text + + def apply_transformation( + self, transformation_input: TransformationInput + ) -> Transformation: + + ( + buffer_control, + document, + lineno, + source_to_display, + fragments, + _, + _, + ) = transformation_input.unpack() + + search_text = self._get_search_text(buffer_control) + searchmatch_fragment = " class:%s " % (self._classname,) + searchmatch_current_fragment = " class:%s " % (self._classname_current,) + + if search_text and not get_app().is_done: + # For each search match, replace the style string. + line_text = fragment_list_to_text(fragments) + fragments = explode_text_fragments(fragments) + + if buffer_control.search_state.ignore_case(): + flags = re.IGNORECASE + else: + flags = re.RegexFlag(0) + + # Get cursor column. + cursor_column: Optional[int] + if document.cursor_position_row == lineno: + cursor_column = source_to_display(document.cursor_position_col) + else: + cursor_column = None + + for match in re.finditer(re.escape(search_text), line_text, flags=flags): + if cursor_column is not None: + on_cursor = match.start() <= cursor_column < match.end() + else: + on_cursor = False + + for i in range(match.start(), match.end()): + old_fragment, text, *_ = fragments[i] + if on_cursor: + fragments[i] = ( + old_fragment + searchmatch_current_fragment, + fragments[i][1], + ) + else: + fragments[i] = ( + old_fragment + searchmatch_fragment, + fragments[i][1], + ) + + return Transformation(fragments) + + +class HighlightIncrementalSearchProcessor(HighlightSearchProcessor): + """ + Highlight the search terms that are used for highlighting the incremental + search. The style class 'incsearch' will be applied to the content. + + Important: this requires the `preview_search=True` flag to be set for the + `BufferControl`. Otherwise, the cursor position won't be set to the search + match while searching, and nothing happens. + """ + + _classname = "incsearch" + _classname_current = "incsearch.current" + + def _get_search_text(self, buffer_control: "BufferControl") -> str: + """ + The text we are searching for. + """ + # When the search buffer has focus, take that text. + search_buffer = buffer_control.search_buffer + if search_buffer is not None and search_buffer.text: + return search_buffer.text + return "" + + +class HighlightSelectionProcessor(Processor): + """ + Processor that highlights the selection in the document. + """ + + def apply_transformation( + self, transformation_input: TransformationInput + ) -> Transformation: + ( + buffer_control, + document, + lineno, + source_to_display, + fragments, + _, + _, + ) = transformation_input.unpack() + + selected_fragment = " class:selected " + + # In case of selection, highlight all matches. + selection_at_line = document.selection_range_at_line(lineno) + + if selection_at_line: + from_, to = selection_at_line + from_ = source_to_display(from_) + to = source_to_display(to) + + fragments = explode_text_fragments(fragments) + + if from_ == 0 and to == 0 and len(fragments) == 0: + # When this is an empty line, insert a space in order to + # visualise the selection. + return Transformation([(selected_fragment, " ")]) + else: + for i in range(from_, to): + if i < len(fragments): + old_fragment, old_text, *_ = fragments[i] + fragments[i] = (old_fragment + selected_fragment, old_text) + elif i == len(fragments): + fragments.append((selected_fragment, " ")) + + return Transformation(fragments) + + +class PasswordProcessor(Processor): + """ + Processor that masks the input. (For passwords.) + + :param char: (string) Character to be used. "*" by default. + """ + + def __init__(self, char: str = "*") -> None: + self.char = char + + def apply_transformation(self, ti: TransformationInput) -> Transformation: + fragments: StyleAndTextTuples = cast( + StyleAndTextTuples, + [ + (style, self.char * len(text), *handler) + for style, text, *handler in ti.fragments + ], + ) + + return Transformation(fragments) + + +class HighlightMatchingBracketProcessor(Processor): + """ + When the cursor is on or right after a bracket, it highlights the matching + bracket. + + :param max_cursor_distance: Only highlight matching brackets when the + cursor is within this distance. (From inside a `Processor`, we can't + know which lines will be visible on the screen. But we also don't want + to scan the whole document for matching brackets on each key press, so + we limit to this value.) + """ + + _closing_braces = "])}>" + + def __init__( + self, chars: str = "[](){}<>", max_cursor_distance: int = 1000 + ) -> None: + self.chars = chars + self.max_cursor_distance = max_cursor_distance + + self._positions_cache: SimpleCache[ + Hashable, List[Tuple[int, int]] + ] = SimpleCache(maxsize=8) + + def _get_positions_to_highlight(self, document: Document) -> List[Tuple[int, int]]: + """ + Return a list of (row, col) tuples that need to be highlighted. + """ + pos: Optional[int] + + # Try for the character under the cursor. + if document.current_char and document.current_char in self.chars: + pos = document.find_matching_bracket_position( + start_pos=document.cursor_position - self.max_cursor_distance, + end_pos=document.cursor_position + self.max_cursor_distance, + ) + + # Try for the character before the cursor. + elif ( + document.char_before_cursor + and document.char_before_cursor in self._closing_braces + and document.char_before_cursor in self.chars + ): + document = Document(document.text, document.cursor_position - 1) + + pos = document.find_matching_bracket_position( + start_pos=document.cursor_position - self.max_cursor_distance, + end_pos=document.cursor_position + self.max_cursor_distance, + ) + else: + pos = None + + # Return a list of (row, col) tuples that need to be highlighted. + if pos: + pos += document.cursor_position # pos is relative. + row, col = document.translate_index_to_position(pos) + return [ + (row, col), + (document.cursor_position_row, document.cursor_position_col), + ] + else: + return [] + + def apply_transformation( + self, transformation_input: TransformationInput + ) -> Transformation: + + ( + buffer_control, + document, + lineno, + source_to_display, + fragments, + _, + _, + ) = transformation_input.unpack() + + # When the application is in the 'done' state, don't highlight. + if get_app().is_done: + return Transformation(fragments) + + # Get the highlight positions. + key = (get_app().render_counter, document.text, document.cursor_position) + positions = self._positions_cache.get( + key, lambda: self._get_positions_to_highlight(document) + ) + + # Apply if positions were found at this line. + if positions: + for row, col in positions: + if row == lineno: + col = source_to_display(col) + fragments = explode_text_fragments(fragments) + style, text, *_ = fragments[col] + + if col == document.cursor_position_col: + style += " class:matching-bracket.cursor " + else: + style += " class:matching-bracket.other " + + fragments[col] = (style, text) + + return Transformation(fragments) + + +class DisplayMultipleCursors(Processor): + """ + When we're in Vi block insert mode, display all the cursors. + """ + + def apply_transformation( + self, transformation_input: TransformationInput + ) -> Transformation: + + ( + buffer_control, + document, + lineno, + source_to_display, + fragments, + _, + _, + ) = transformation_input.unpack() + + buff = buffer_control.buffer + + if vi_insert_multiple_mode(): + cursor_positions = buff.multiple_cursor_positions + fragments = explode_text_fragments(fragments) + + # If any cursor appears on the current line, highlight that. + start_pos = document.translate_row_col_to_index(lineno, 0) + end_pos = start_pos + len(document.lines[lineno]) + + fragment_suffix = " class:multiple-cursors" + + for p in cursor_positions: + if start_pos <= p <= end_pos: + column = source_to_display(p - start_pos) + + # Replace fragment. + try: + style, text, *_ = fragments[column] + except IndexError: + # Cursor needs to be displayed after the current text. + fragments.append((fragment_suffix, " ")) + else: + style += fragment_suffix + fragments[column] = (style, text) + + return Transformation(fragments) + else: + return Transformation(fragments) + + +class BeforeInput(Processor): + """ + Insert text before the input. + + :param text: This can be either plain text or formatted text + (or a callable that returns any of those). + :param style: style to be applied to this prompt/prefix. + """ + + def __init__(self, text: AnyFormattedText, style: str = "") -> None: + self.text = text + self.style = style + + def apply_transformation(self, ti: TransformationInput) -> Transformation: + source_to_display: Optional[SourceToDisplay] + display_to_source: Optional[DisplayToSource] + + if ti.lineno == 0: + # Get fragments. + fragments_before = to_formatted_text(self.text, self.style) + fragments = fragments_before + ti.fragments + + shift_position = fragment_list_len(fragments_before) + source_to_display = lambda i: i + shift_position + display_to_source = lambda i: i - shift_position + else: + fragments = ti.fragments + source_to_display = None + display_to_source = None + + return Transformation( + fragments, + source_to_display=source_to_display, + display_to_source=display_to_source, + ) + + def __repr__(self) -> str: + return "BeforeInput(%r, %r)" % (self.text, self.style) + + +class ShowArg(BeforeInput): + """ + Display the 'arg' in front of the input. + + This was used by the `PromptSession`, but now it uses the + `Window.get_line_prefix` function instead. + """ + + def __init__(self) -> None: + super().__init__(self._get_text_fragments) + + def _get_text_fragments(self) -> StyleAndTextTuples: + app = get_app() + if app.key_processor.arg is None: + return [] + else: + arg = app.key_processor.arg + + return [ + ("class:prompt.arg", "(arg: "), + ("class:prompt.arg.text", str(arg)), + ("class:prompt.arg", ") "), + ] + + def __repr__(self) -> str: + return "ShowArg()" + + +class AfterInput(Processor): + """ + Insert text after the input. + + :param text: This can be either plain text or formatted text + (or a callable that returns any of those). + :param style: style to be applied to this prompt/prefix. + """ + + def __init__(self, text: AnyFormattedText, style: str = "") -> None: + self.text = text + self.style = style + + def apply_transformation(self, ti: TransformationInput) -> Transformation: + # Insert fragments after the last line. + if ti.lineno == ti.document.line_count - 1: + # Get fragments. + fragments_after = to_formatted_text(self.text, self.style) + return Transformation(fragments=ti.fragments + fragments_after) + else: + return Transformation(fragments=ti.fragments) + + def __repr__(self) -> str: + return "%s(%r, style=%r)" % (self.__class__.__name__, self.text, self.style) + + +class AppendAutoSuggestion(Processor): + """ + Append the auto suggestion to the input. + (The user can then press the right arrow the insert the suggestion.) + """ + + def __init__(self, style: str = "class:auto-suggestion") -> None: + self.style = style + + def apply_transformation(self, ti: TransformationInput) -> Transformation: + # Insert fragments after the last line. + if ti.lineno == ti.document.line_count - 1: + buffer = ti.buffer_control.buffer + + if buffer.suggestion and ti.document.is_cursor_at_the_end: + suggestion = buffer.suggestion.text + else: + suggestion = "" + + return Transformation(fragments=ti.fragments + [(self.style, suggestion)]) + else: + return Transformation(fragments=ti.fragments) + + +class ShowLeadingWhiteSpaceProcessor(Processor): + """ + Make leading whitespace visible. + + :param get_char: Callable that returns one character. + """ + + def __init__( + self, + get_char: Optional[Callable[[], str]] = None, + style: str = "class:leading-whitespace", + ) -> None: + def default_get_char() -> str: + if "\xb7".encode(get_app().output.encoding(), "replace") == b"?": + return "." + else: + return "\xb7" + + self.style = style + self.get_char = get_char or default_get_char + + def apply_transformation(self, ti: TransformationInput) -> Transformation: + fragments = ti.fragments + + # Walk through all te fragments. + if fragments and fragment_list_to_text(fragments).startswith(" "): + t = (self.style, self.get_char()) + fragments = explode_text_fragments(fragments) + + for i in range(len(fragments)): + if fragments[i][1] == " ": + fragments[i] = t + else: + break + + return Transformation(fragments) + + +class ShowTrailingWhiteSpaceProcessor(Processor): + """ + Make trailing whitespace visible. + + :param get_char: Callable that returns one character. + """ + + def __init__( + self, + get_char: Optional[Callable[[], str]] = None, + style: str = "class:training-whitespace", + ) -> None: + def default_get_char() -> str: + if "\xb7".encode(get_app().output.encoding(), "replace") == b"?": + return "." + else: + return "\xb7" + + self.style = style + self.get_char = get_char or default_get_char + + def apply_transformation(self, ti: TransformationInput) -> Transformation: + fragments = ti.fragments + + if fragments and fragments[-1][1].endswith(" "): + t = (self.style, self.get_char()) + fragments = explode_text_fragments(fragments) + + # Walk backwards through all te fragments and replace whitespace. + for i in range(len(fragments) - 1, -1, -1): + char = fragments[i][1] + if char == " ": + fragments[i] = t + else: + break + + return Transformation(fragments) + + +class TabsProcessor(Processor): + """ + Render tabs as spaces (instead of ^I) or make them visible (for instance, + by replacing them with dots.) + + :param tabstop: Horizontal space taken by a tab. (`int` or callable that + returns an `int`). + :param char1: Character or callable that returns a character (text of + length one). This one is used for the first space taken by the tab. + :param char2: Like `char1`, but for the rest of the space. + """ + + def __init__( + self, + tabstop: Union[int, Callable[[], int]] = 4, + char1: Union[str, Callable[[], str]] = "|", + char2: Union[str, Callable[[], str]] = "\u2508", + style: str = "class:tab", + ) -> None: + + self.char1 = char1 + self.char2 = char2 + self.tabstop = tabstop + self.style = style + + def apply_transformation(self, ti: TransformationInput) -> Transformation: + tabstop = to_int(self.tabstop) + style = self.style + + # Create separator for tabs. + separator1 = to_str(self.char1) + separator2 = to_str(self.char2) + + # Transform fragments. + fragments = explode_text_fragments(ti.fragments) + + position_mappings = {} + result_fragments: StyleAndTextTuples = [] + pos = 0 + + for i, fragment_and_text in enumerate(fragments): + position_mappings[i] = pos + + if fragment_and_text[1] == "\t": + # Calculate how many characters we have to insert. + count = tabstop - (pos % tabstop) + if count == 0: + count = tabstop + + # Insert tab. + result_fragments.append((style, separator1)) + result_fragments.append((style, separator2 * (count - 1))) + pos += count + else: + result_fragments.append(fragment_and_text) + pos += 1 + + position_mappings[len(fragments)] = pos + # Add `pos+1` to mapping, because the cursor can be right after the + # line as well. + position_mappings[len(fragments) + 1] = pos + 1 + + def source_to_display(from_position: int) -> int: + "Maps original cursor position to the new one." + return position_mappings[from_position] + + def display_to_source(display_pos: int) -> int: + "Maps display cursor position to the original one." + position_mappings_reversed = {v: k for k, v in position_mappings.items()} + + while display_pos >= 0: + try: + return position_mappings_reversed[display_pos] + except KeyError: + display_pos -= 1 + return 0 + + return Transformation( + result_fragments, + source_to_display=source_to_display, + display_to_source=display_to_source, + ) + + +class ReverseSearchProcessor(Processor): + """ + Process to display the "(reverse-i-search)`...`:..." stuff around + the search buffer. + + Note: This processor is meant to be applied to the BufferControl that + contains the search buffer, it's not meant for the original input. + """ + + _excluded_input_processors: List[Type[Processor]] = [ + HighlightSearchProcessor, + HighlightSelectionProcessor, + BeforeInput, + AfterInput, + ] + + def _get_main_buffer( + self, buffer_control: "BufferControl" + ) -> Optional["BufferControl"]: + from prompt_toolkit.layout.controls import BufferControl + + prev_control = get_app().layout.search_target_buffer_control + if ( + isinstance(prev_control, BufferControl) + and prev_control.search_buffer_control == buffer_control + ): + return prev_control + return None + + def _content( + self, main_control: "BufferControl", ti: TransformationInput + ) -> "UIContent": + from prompt_toolkit.layout.controls import BufferControl + + # Emulate the BufferControl through which we are searching. + # For this we filter out some of the input processors. + excluded_processors = tuple(self._excluded_input_processors) + + def filter_processor(item: Processor) -> Optional[Processor]: + """Filter processors from the main control that we want to disable + here. This returns either an accepted processor or None.""" + # For a `_MergedProcessor`, check each individual processor, recursively. + if isinstance(item, _MergedProcessor): + accepted_processors = [filter_processor(p) for p in item.processors] + return merge_processors( + [p for p in accepted_processors if p is not None] + ) + + # For a `ConditionalProcessor`, check the body. + elif isinstance(item, ConditionalProcessor): + p = filter_processor(item.processor) + if p: + return ConditionalProcessor(p, item.filter) + + # Otherwise, check the processor itself. + else: + if not isinstance(item, excluded_processors): + return item + + return None + + filtered_processor = filter_processor( + merge_processors(main_control.input_processors or []) + ) + highlight_processor = HighlightIncrementalSearchProcessor() + + if filtered_processor: + new_processors = [filtered_processor, highlight_processor] + else: + new_processors = [highlight_processor] + + from .controls import SearchBufferControl + + assert isinstance(ti.buffer_control, SearchBufferControl) + + buffer_control = BufferControl( + buffer=main_control.buffer, + input_processors=new_processors, + include_default_input_processors=False, + lexer=main_control.lexer, + preview_search=True, + search_buffer_control=ti.buffer_control, + ) + + return buffer_control.create_content(ti.width, ti.height, preview_search=True) + + def apply_transformation(self, ti: TransformationInput) -> Transformation: + from .controls import SearchBufferControl + + assert isinstance( + ti.buffer_control, SearchBufferControl + ), "`ReverseSearchProcessor` should be applied to a `SearchBufferControl` only." + + source_to_display: Optional[SourceToDisplay] + display_to_source: Optional[DisplayToSource] + + main_control = self._get_main_buffer(ti.buffer_control) + + if ti.lineno == 0 and main_control: + content = self._content(main_control, ti) + + # Get the line from the original document for this search. + line_fragments = content.get_line(content.cursor_position.y) + + if main_control.search_state.direction == SearchDirection.FORWARD: + direction_text = "i-search" + else: + direction_text = "reverse-i-search" + + fragments_before: StyleAndTextTuples = [ + ("class:prompt.search", "("), + ("class:prompt.search", direction_text), + ("class:prompt.search", ")`"), + ] + + fragments = ( + fragments_before + + [ + ("class:prompt.search.text", fragment_list_to_text(ti.fragments)), + ("", "': "), + ] + + line_fragments + ) + + shift_position = fragment_list_len(fragments_before) + source_to_display = lambda i: i + shift_position + display_to_source = lambda i: i - shift_position + else: + source_to_display = None + display_to_source = None + fragments = ti.fragments + + return Transformation( + fragments, + source_to_display=source_to_display, + display_to_source=display_to_source, + ) + + +class ConditionalProcessor(Processor): + """ + Processor that applies another processor, according to a certain condition. + Example:: + + # Create a function that returns whether or not the processor should + # currently be applied. + def highlight_enabled(): + return true_or_false + + # Wrapped it in a `ConditionalProcessor` for usage in a `BufferControl`. + BufferControl(input_processors=[ + ConditionalProcessor(HighlightSearchProcessor(), + Condition(highlight_enabled))]) + + :param processor: :class:`.Processor` instance. + :param filter: :class:`~prompt_toolkit.filters.Filter` instance. + """ + + def __init__(self, processor: Processor, filter: FilterOrBool) -> None: + self.processor = processor + self.filter = to_filter(filter) + + def apply_transformation( + self, transformation_input: TransformationInput + ) -> Transformation: + # Run processor when enabled. + if self.filter(): + return self.processor.apply_transformation(transformation_input) + else: + return Transformation(transformation_input.fragments) + + def __repr__(self) -> str: + return "%s(processor=%r, filter=%r)" % ( + self.__class__.__name__, + self.processor, + self.filter, + ) + + +class DynamicProcessor(Processor): + """ + Processor class that dynamically returns any Processor. + + :param get_processor: Callable that returns a :class:`.Processor` instance. + """ + + def __init__(self, get_processor: Callable[[], Optional[Processor]]) -> None: + self.get_processor = get_processor + + def apply_transformation(self, ti: TransformationInput) -> Transformation: + processor = self.get_processor() or DummyProcessor() + return processor.apply_transformation(ti) + + +def merge_processors(processors: List[Processor]) -> Processor: + """ + Merge multiple `Processor` objects into one. + """ + if len(processors) == 0: + return DummyProcessor() + + if len(processors) == 1: + return processors[0] # Nothing to merge. + + return _MergedProcessor(processors) + + +class _MergedProcessor(Processor): + """ + Processor that groups multiple other `Processor` objects, but exposes an + API as if it is one `Processor`. + """ + + def __init__(self, processors: List[Processor]): + self.processors = processors + + def apply_transformation(self, ti: TransformationInput) -> Transformation: + source_to_display_functions = [ti.source_to_display] + display_to_source_functions = [] + fragments = ti.fragments + + def source_to_display(i: int) -> int: + """Translate x position from the buffer to the x position in the + processor fragments list.""" + for f in source_to_display_functions: + i = f(i) + return i + + for p in self.processors: + transformation = p.apply_transformation( + TransformationInput( + ti.buffer_control, + ti.document, + ti.lineno, + source_to_display, + fragments, + ti.width, + ti.height, + ) + ) + fragments = transformation.fragments + display_to_source_functions.append(transformation.display_to_source) + source_to_display_functions.append(transformation.source_to_display) + + def display_to_source(i: int) -> int: + for f in reversed(display_to_source_functions): + i = f(i) + return i + + # In the case of a nested _MergedProcessor, each processor wants to + # receive a 'source_to_display' function (as part of the + # TransformationInput) that has everything in the chain before + # included, because it can be called as part of the + # `apply_transformation` function. However, this first + # `source_to_display` should not be part of the output that we are + # returning. (This is the most consistent with `display_to_source`.) + del source_to_display_functions[:1] + + return Transformation(fragments, source_to_display, display_to_source) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/screen.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/screen.py index aad253e8ad..deb2f43222 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/screen.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/screen.py @@ -1,327 +1,327 @@ -from collections import defaultdict -from typing import TYPE_CHECKING, Callable, DefaultDict, Dict, List, Optional, Tuple - -from prompt_toolkit.cache import FastDictCache -from prompt_toolkit.data_structures import Point -from prompt_toolkit.utils import get_cwidth - -if TYPE_CHECKING: - from .containers import Window - - -__all__ = [ - "Screen", - "Char", -] - - -class Char: - """ - Represent a single character in a :class:`.Screen`. - - This should be considered immutable. - - :param char: A single character (can be a double-width character). - :param style: A style string. (Can contain classnames.) - """ - - __slots__ = ("char", "style", "width") - - # If we end up having one of these special control sequences in the input string, - # we should display them as follows: - # Usually this happens after a "quoted insert". - display_mappings: Dict[str, str] = { - "\x00": "^@", # Control space - "\x01": "^A", - "\x02": "^B", - "\x03": "^C", - "\x04": "^D", - "\x05": "^E", - "\x06": "^F", - "\x07": "^G", - "\x08": "^H", - "\x09": "^I", - "\x0a": "^J", - "\x0b": "^K", - "\x0c": "^L", - "\x0d": "^M", - "\x0e": "^N", - "\x0f": "^O", - "\x10": "^P", - "\x11": "^Q", - "\x12": "^R", - "\x13": "^S", - "\x14": "^T", - "\x15": "^U", - "\x16": "^V", - "\x17": "^W", - "\x18": "^X", - "\x19": "^Y", - "\x1a": "^Z", - "\x1b": "^[", # Escape - "\x1c": "^\\", - "\x1d": "^]", - "\x1f": "^_", - "\x7f": "^?", # ASCII Delete (backspace). - # Special characters. All visualized like Vim does. - "\x80": "<80>", - "\x81": "<81>", - "\x82": "<82>", - "\x83": "<83>", - "\x84": "<84>", - "\x85": "<85>", - "\x86": "<86>", - "\x87": "<87>", - "\x88": "<88>", - "\x89": "<89>", - "\x8a": "<8a>", - "\x8b": "<8b>", - "\x8c": "<8c>", - "\x8d": "<8d>", - "\x8e": "<8e>", - "\x8f": "<8f>", - "\x90": "<90>", - "\x91": "<91>", - "\x92": "<92>", - "\x93": "<93>", - "\x94": "<94>", - "\x95": "<95>", - "\x96": "<96>", - "\x97": "<97>", - "\x98": "<98>", - "\x99": "<99>", - "\x9a": "<9a>", - "\x9b": "<9b>", - "\x9c": "<9c>", - "\x9d": "<9d>", - "\x9e": "<9e>", - "\x9f": "<9f>", - # For the non-breaking space: visualize like Emacs does by default. - # (Print a space, but attach the 'nbsp' class that applies the - # underline style.) - "\xa0": " ", - } - - def __init__(self, char: str = " ", style: str = "") -> None: - # If this character has to be displayed otherwise, take that one. - if char in self.display_mappings: - if char == "\xa0": - style += " class:nbsp " # Will be underlined. - else: - style += " class:control-character " - - char = self.display_mappings[char] - - self.char = char - self.style = style - - # Calculate width. (We always need this, so better to store it directly - # as a member for performance.) - self.width = get_cwidth(char) - - # In theory, `other` can be any type of object, but because of performance - # we don't want to do an `isinstance` check every time. We assume "other" - # is always a "Char". - def _equal(self, other: "Char") -> bool: - return self.char == other.char and self.style == other.style - - def _not_equal(self, other: "Char") -> bool: - # Not equal: We don't do `not char.__eq__` here, because of the - # performance of calling yet another function. - return self.char != other.char or self.style != other.style - - if not TYPE_CHECKING: - __eq__ = _equal - __ne__ = _not_equal - - def __repr__(self) -> str: - return "%s(%r, %r)" % (self.__class__.__name__, self.char, self.style) - - -_CHAR_CACHE: FastDictCache[Tuple[str, str], Char] = FastDictCache( - Char, size=1000 * 1000 -) -Transparent = "[transparent]" - - -class Screen: - """ - Two dimensional buffer of :class:`.Char` instances. - """ - - def __init__( - self, - default_char: Optional[Char] = None, - initial_width: int = 0, - initial_height: int = 0, - ) -> None: - - if default_char is None: - default_char2 = _CHAR_CACHE[" ", Transparent] - else: - default_char2 = default_char - - self.data_buffer: DefaultDict[int, DefaultDict[int, Char]] = defaultdict( - lambda: defaultdict(lambda: default_char2) - ) - - #: Escape sequences to be injected. - self.zero_width_escapes: DefaultDict[int, DefaultDict[int, str]] = defaultdict( - lambda: defaultdict(lambda: "") - ) - - #: Position of the cursor. - self.cursor_positions: Dict[ - "Window", Point - ] = {} # Map `Window` objects to `Point` objects. - - #: Visibility of the cursor. - self.show_cursor = True - - #: (Optional) Where to position the menu. E.g. at the start of a completion. - #: (We can't use the cursor position, because we don't want the - #: completion menu to change its position when we browse through all the - #: completions.) - self.menu_positions: Dict[ - "Window", Point - ] = {} # Map `Window` objects to `Point` objects. - - #: Currently used width/height of the screen. This will increase when - #: data is written to the screen. - self.width = initial_width or 0 - self.height = initial_height or 0 - - # Windows that have been drawn. (Each `Window` class will add itself to - # this list.) - self.visible_windows_to_write_positions: Dict["Window", "WritePosition"] = {} - - # List of (z_index, draw_func) - self._draw_float_functions: List[Tuple[int, Callable[[], None]]] = [] - - @property - def visible_windows(self) -> List["Window"]: - return list(self.visible_windows_to_write_positions.keys()) - - def set_cursor_position(self, window: "Window", position: Point) -> None: - """ - Set the cursor position for a given window. - """ - self.cursor_positions[window] = position - - def set_menu_position(self, window: "Window", position: Point) -> None: - """ - Set the cursor position for a given window. - """ - self.menu_positions[window] = position - - def get_cursor_position(self, window: "Window") -> Point: - """ - Get the cursor position for a given window. - Returns a `Point`. - """ - try: - return self.cursor_positions[window] - except KeyError: - return Point(x=0, y=0) - - def get_menu_position(self, window: "Window") -> Point: - """ - Get the menu position for a given window. - (This falls back to the cursor position if no menu position was set.) - """ - try: - return self.menu_positions[window] - except KeyError: - try: - return self.cursor_positions[window] - except KeyError: - return Point(x=0, y=0) - - def draw_with_z_index(self, z_index: int, draw_func: Callable[[], None]) -> None: - """ - Add a draw-function for a `Window` which has a >= 0 z_index. - This will be postponed until `draw_all_floats` is called. - """ - self._draw_float_functions.append((z_index, draw_func)) - - def draw_all_floats(self) -> None: - """ - Draw all float functions in order of z-index. - """ - # We keep looping because some draw functions could add new functions - # to this list. See `FloatContainer`. - while self._draw_float_functions: - # Sort the floats that we have so far by z_index. - functions = sorted(self._draw_float_functions, key=lambda item: item[0]) - - # Draw only one at a time, then sort everything again. Now floats - # might have been added. - self._draw_float_functions = functions[1:] - functions[0][1]() - - def append_style_to_content(self, style_str: str) -> None: - """ - For all the characters in the screen. - Set the style string to the given `style_str`. - """ - b = self.data_buffer - char_cache = _CHAR_CACHE - - append_style = " " + style_str - - for y, row in b.items(): - for x, char in row.items(): - b[y][x] = char_cache[char.char, char.style + append_style] - - def fill_area( - self, write_position: "WritePosition", style: str = "", after: bool = False - ) -> None: - """ - Fill the content of this area, using the given `style`. - The style is prepended before whatever was here before. - """ - if not style.strip(): - return - - xmin = write_position.xpos - xmax = write_position.xpos + write_position.width - char_cache = _CHAR_CACHE - data_buffer = self.data_buffer - - if after: - append_style = " " + style - prepend_style = "" - else: - append_style = "" - prepend_style = style + " " - - for y in range( - write_position.ypos, write_position.ypos + write_position.height - ): - row = data_buffer[y] - for x in range(xmin, xmax): - cell = row[x] - row[x] = char_cache[ - cell.char, prepend_style + cell.style + append_style - ] - - -class WritePosition: - def __init__(self, xpos: int, ypos: int, width: int, height: int) -> None: - assert height >= 0 - assert width >= 0 - # xpos and ypos can be negative. (A float can be partially visible.) - - self.xpos = xpos - self.ypos = ypos - self.width = width - self.height = height - - def __repr__(self) -> str: - return "%s(x=%r, y=%r, width=%r, height=%r)" % ( - self.__class__.__name__, - self.xpos, - self.ypos, - self.width, - self.height, - ) +from collections import defaultdict +from typing import TYPE_CHECKING, Callable, DefaultDict, Dict, List, Optional, Tuple + +from prompt_toolkit.cache import FastDictCache +from prompt_toolkit.data_structures import Point +from prompt_toolkit.utils import get_cwidth + +if TYPE_CHECKING: + from .containers import Window + + +__all__ = [ + "Screen", + "Char", +] + + +class Char: + """ + Represent a single character in a :class:`.Screen`. + + This should be considered immutable. + + :param char: A single character (can be a double-width character). + :param style: A style string. (Can contain classnames.) + """ + + __slots__ = ("char", "style", "width") + + # If we end up having one of these special control sequences in the input string, + # we should display them as follows: + # Usually this happens after a "quoted insert". + display_mappings: Dict[str, str] = { + "\x00": "^@", # Control space + "\x01": "^A", + "\x02": "^B", + "\x03": "^C", + "\x04": "^D", + "\x05": "^E", + "\x06": "^F", + "\x07": "^G", + "\x08": "^H", + "\x09": "^I", + "\x0a": "^J", + "\x0b": "^K", + "\x0c": "^L", + "\x0d": "^M", + "\x0e": "^N", + "\x0f": "^O", + "\x10": "^P", + "\x11": "^Q", + "\x12": "^R", + "\x13": "^S", + "\x14": "^T", + "\x15": "^U", + "\x16": "^V", + "\x17": "^W", + "\x18": "^X", + "\x19": "^Y", + "\x1a": "^Z", + "\x1b": "^[", # Escape + "\x1c": "^\\", + "\x1d": "^]", + "\x1f": "^_", + "\x7f": "^?", # ASCII Delete (backspace). + # Special characters. All visualized like Vim does. + "\x80": "<80>", + "\x81": "<81>", + "\x82": "<82>", + "\x83": "<83>", + "\x84": "<84>", + "\x85": "<85>", + "\x86": "<86>", + "\x87": "<87>", + "\x88": "<88>", + "\x89": "<89>", + "\x8a": "<8a>", + "\x8b": "<8b>", + "\x8c": "<8c>", + "\x8d": "<8d>", + "\x8e": "<8e>", + "\x8f": "<8f>", + "\x90": "<90>", + "\x91": "<91>", + "\x92": "<92>", + "\x93": "<93>", + "\x94": "<94>", + "\x95": "<95>", + "\x96": "<96>", + "\x97": "<97>", + "\x98": "<98>", + "\x99": "<99>", + "\x9a": "<9a>", + "\x9b": "<9b>", + "\x9c": "<9c>", + "\x9d": "<9d>", + "\x9e": "<9e>", + "\x9f": "<9f>", + # For the non-breaking space: visualize like Emacs does by default. + # (Print a space, but attach the 'nbsp' class that applies the + # underline style.) + "\xa0": " ", + } + + def __init__(self, char: str = " ", style: str = "") -> None: + # If this character has to be displayed otherwise, take that one. + if char in self.display_mappings: + if char == "\xa0": + style += " class:nbsp " # Will be underlined. + else: + style += " class:control-character " + + char = self.display_mappings[char] + + self.char = char + self.style = style + + # Calculate width. (We always need this, so better to store it directly + # as a member for performance.) + self.width = get_cwidth(char) + + # In theory, `other` can be any type of object, but because of performance + # we don't want to do an `isinstance` check every time. We assume "other" + # is always a "Char". + def _equal(self, other: "Char") -> bool: + return self.char == other.char and self.style == other.style + + def _not_equal(self, other: "Char") -> bool: + # Not equal: We don't do `not char.__eq__` here, because of the + # performance of calling yet another function. + return self.char != other.char or self.style != other.style + + if not TYPE_CHECKING: + __eq__ = _equal + __ne__ = _not_equal + + def __repr__(self) -> str: + return "%s(%r, %r)" % (self.__class__.__name__, self.char, self.style) + + +_CHAR_CACHE: FastDictCache[Tuple[str, str], Char] = FastDictCache( + Char, size=1000 * 1000 +) +Transparent = "[transparent]" + + +class Screen: + """ + Two dimensional buffer of :class:`.Char` instances. + """ + + def __init__( + self, + default_char: Optional[Char] = None, + initial_width: int = 0, + initial_height: int = 0, + ) -> None: + + if default_char is None: + default_char2 = _CHAR_CACHE[" ", Transparent] + else: + default_char2 = default_char + + self.data_buffer: DefaultDict[int, DefaultDict[int, Char]] = defaultdict( + lambda: defaultdict(lambda: default_char2) + ) + + #: Escape sequences to be injected. + self.zero_width_escapes: DefaultDict[int, DefaultDict[int, str]] = defaultdict( + lambda: defaultdict(lambda: "") + ) + + #: Position of the cursor. + self.cursor_positions: Dict[ + "Window", Point + ] = {} # Map `Window` objects to `Point` objects. + + #: Visibility of the cursor. + self.show_cursor = True + + #: (Optional) Where to position the menu. E.g. at the start of a completion. + #: (We can't use the cursor position, because we don't want the + #: completion menu to change its position when we browse through all the + #: completions.) + self.menu_positions: Dict[ + "Window", Point + ] = {} # Map `Window` objects to `Point` objects. + + #: Currently used width/height of the screen. This will increase when + #: data is written to the screen. + self.width = initial_width or 0 + self.height = initial_height or 0 + + # Windows that have been drawn. (Each `Window` class will add itself to + # this list.) + self.visible_windows_to_write_positions: Dict["Window", "WritePosition"] = {} + + # List of (z_index, draw_func) + self._draw_float_functions: List[Tuple[int, Callable[[], None]]] = [] + + @property + def visible_windows(self) -> List["Window"]: + return list(self.visible_windows_to_write_positions.keys()) + + def set_cursor_position(self, window: "Window", position: Point) -> None: + """ + Set the cursor position for a given window. + """ + self.cursor_positions[window] = position + + def set_menu_position(self, window: "Window", position: Point) -> None: + """ + Set the cursor position for a given window. + """ + self.menu_positions[window] = position + + def get_cursor_position(self, window: "Window") -> Point: + """ + Get the cursor position for a given window. + Returns a `Point`. + """ + try: + return self.cursor_positions[window] + except KeyError: + return Point(x=0, y=0) + + def get_menu_position(self, window: "Window") -> Point: + """ + Get the menu position for a given window. + (This falls back to the cursor position if no menu position was set.) + """ + try: + return self.menu_positions[window] + except KeyError: + try: + return self.cursor_positions[window] + except KeyError: + return Point(x=0, y=0) + + def draw_with_z_index(self, z_index: int, draw_func: Callable[[], None]) -> None: + """ + Add a draw-function for a `Window` which has a >= 0 z_index. + This will be postponed until `draw_all_floats` is called. + """ + self._draw_float_functions.append((z_index, draw_func)) + + def draw_all_floats(self) -> None: + """ + Draw all float functions in order of z-index. + """ + # We keep looping because some draw functions could add new functions + # to this list. See `FloatContainer`. + while self._draw_float_functions: + # Sort the floats that we have so far by z_index. + functions = sorted(self._draw_float_functions, key=lambda item: item[0]) + + # Draw only one at a time, then sort everything again. Now floats + # might have been added. + self._draw_float_functions = functions[1:] + functions[0][1]() + + def append_style_to_content(self, style_str: str) -> None: + """ + For all the characters in the screen. + Set the style string to the given `style_str`. + """ + b = self.data_buffer + char_cache = _CHAR_CACHE + + append_style = " " + style_str + + for y, row in b.items(): + for x, char in row.items(): + b[y][x] = char_cache[char.char, char.style + append_style] + + def fill_area( + self, write_position: "WritePosition", style: str = "", after: bool = False + ) -> None: + """ + Fill the content of this area, using the given `style`. + The style is prepended before whatever was here before. + """ + if not style.strip(): + return + + xmin = write_position.xpos + xmax = write_position.xpos + write_position.width + char_cache = _CHAR_CACHE + data_buffer = self.data_buffer + + if after: + append_style = " " + style + prepend_style = "" + else: + append_style = "" + prepend_style = style + " " + + for y in range( + write_position.ypos, write_position.ypos + write_position.height + ): + row = data_buffer[y] + for x in range(xmin, xmax): + cell = row[x] + row[x] = char_cache[ + cell.char, prepend_style + cell.style + append_style + ] + + +class WritePosition: + def __init__(self, xpos: int, ypos: int, width: int, height: int) -> None: + assert height >= 0 + assert width >= 0 + # xpos and ypos can be negative. (A float can be partially visible.) + + self.xpos = xpos + self.ypos = ypos + self.width = width + self.height = height + + def __repr__(self) -> str: + return "%s(x=%r, y=%r, width=%r, height=%r)" % ( + self.__class__.__name__, + self.xpos, + self.ypos, + self.width, + self.height, + ) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/scrollable_pane.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/scrollable_pane.py index 57d813d511..a5500d7f7c 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/scrollable_pane.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/scrollable_pane.py @@ -1,493 +1,493 @@ -from typing import Dict, List, Optional - -from prompt_toolkit.data_structures import Point -from prompt_toolkit.filters import FilterOrBool, to_filter -from prompt_toolkit.key_binding import KeyBindingsBase -from prompt_toolkit.mouse_events import MouseEvent - -from .containers import Container, ScrollOffsets -from .dimension import AnyDimension, Dimension, sum_layout_dimensions, to_dimension -from .mouse_handlers import MouseHandler, MouseHandlers -from .screen import Char, Screen, WritePosition - -__all__ = ["ScrollablePane"] - -# Never go beyond this height, because performance will degrade. -MAX_AVAILABLE_HEIGHT = 10_000 - - -class ScrollablePane(Container): - """ - Container widget that exposes a larger virtual screen to its content and - displays it in a vertical scrollbale region. - - Typically this is wrapped in a large `HSplit` container. Make sure in that - case to not specify a `height` dimension of the `HSplit`, so that it will - scale according to the content. - - .. note:: - - If you want to display a completion menu for widgets in this - `ScrollablePane`, then it's still a good practice to use a - `FloatContainer` with a `CompletionsMenu` in a `Float` at the top-level - of the layout hierarchy, rather then nesting a `FloatContainer` in this - `ScrollablePane`. (Otherwise, it's possible that the completion menu - is clipped.) - - :param content: The content container. - :param scrolloffset: Try to keep the cursor within this distance from the - top/bottom (left/right offset is not used). - :param keep_cursor_visible: When `True`, automatically scroll the pane so - that the cursor (of the focused window) is always visible. - :param keep_focused_window_visible: When `True`, automatically scroll th e - pane so that the focused window is visible, or as much visible as - possible if it doen't completely fit the screen. - :param max_available_height: Always constraint the height to this amount - for performance reasons. - :param width: When given, use this width instead of looking at the children. - :param height: When given, use this height instead of looking at the children. - :param show_scrollbar: When `True` display a scrollbar on the right. - """ - - def __init__( - self, - content: Container, - scroll_offsets: Optional[ScrollOffsets] = None, - keep_cursor_visible: FilterOrBool = True, - keep_focused_window_visible: FilterOrBool = True, - max_available_height: int = MAX_AVAILABLE_HEIGHT, - width: AnyDimension = None, - height: AnyDimension = None, - show_scrollbar: FilterOrBool = True, - display_arrows: FilterOrBool = True, - up_arrow_symbol: str = "^", - down_arrow_symbol: str = "v", - ) -> None: - self.content = content - self.scroll_offsets = scroll_offsets or ScrollOffsets(top=1, bottom=1) - self.keep_cursor_visible = to_filter(keep_cursor_visible) - self.keep_focused_window_visible = to_filter(keep_focused_window_visible) - self.max_available_height = max_available_height - self.width = width - self.height = height - self.show_scrollbar = to_filter(show_scrollbar) - self.display_arrows = to_filter(display_arrows) - self.up_arrow_symbol = up_arrow_symbol - self.down_arrow_symbol = down_arrow_symbol - - self.vertical_scroll = 0 - - def __repr__(self) -> str: - return f"ScrollablePane({self.content!r})" - - def reset(self) -> None: - self.content.reset() - - def preferred_width(self, max_available_width: int) -> Dimension: - if self.width is not None: - return to_dimension(self.width) - - # We're only scrolling vertical. So the preferred width is equal to - # that of the content. - content_width = self.content.preferred_width(max_available_width) - - # If a scrollbar needs to be displayed, add +1 to the content width. - if self.show_scrollbar(): - return sum_layout_dimensions([Dimension.exact(1), content_width]) - - return content_width - - def preferred_height(self, width: int, max_available_height: int) -> Dimension: - if self.height is not None: - return to_dimension(self.height) - - # Prefer a height large enough so that it fits all the content. If not, - # we'll make the pane scrollable. - if self.show_scrollbar(): - # If `show_scrollbar` is set. Always reserve space for the scrollbar. - width -= 1 - - dimension = self.content.preferred_height(width, self.max_available_height) - - # Only take 'preferred' into account. Min/max can be anything. - return Dimension(min=0, preferred=dimension.preferred) - - def write_to_screen( - self, - screen: Screen, - mouse_handlers: MouseHandlers, - write_position: WritePosition, - parent_style: str, - erase_bg: bool, - z_index: Optional[int], - ) -> None: - """ - Render scrollable pane content. - - This works by rendering on an off-screen canvas, and copying over the - visible region. - """ - show_scrollbar = self.show_scrollbar() - - if show_scrollbar: - virtual_width = write_position.width - 1 - else: - virtual_width = write_position.width - - # Compute preferred height again. - virtual_height = self.content.preferred_height( - virtual_width, self.max_available_height - ).preferred - - # Ensure virtual height is at least the available height. - virtual_height = max(virtual_height, write_position.height) - virtual_height = min(virtual_height, self.max_available_height) - - # First, write the content to a virtual screen, then copy over the - # visible part to the real screen. - temp_screen = Screen(default_char=Char(char=" ", style=parent_style)) - temp_write_position = WritePosition( - xpos=0, ypos=0, width=virtual_width, height=virtual_height - ) - - temp_mouse_handlers = MouseHandlers() - - self.content.write_to_screen( - temp_screen, - temp_mouse_handlers, - temp_write_position, - parent_style, - erase_bg, - z_index, - ) - temp_screen.draw_all_floats() - - # If anything in the virtual screen is focused, move vertical scroll to - from prompt_toolkit.application import get_app - - focused_window = get_app().layout.current_window - - try: - visible_win_write_pos = temp_screen.visible_windows_to_write_positions[ - focused_window - ] - except KeyError: - pass # No window focused here. Don't scroll. - else: - # Make sure this window is visible. - self._make_window_visible( - write_position.height, - virtual_height, - visible_win_write_pos, - temp_screen.cursor_positions.get(focused_window), - ) - - # Copy over virtual screen and zero width escapes to real screen. - self._copy_over_screen(screen, temp_screen, write_position, virtual_width) - - # Copy over mouse handlers. - self._copy_over_mouse_handlers( - mouse_handlers, temp_mouse_handlers, write_position, virtual_width - ) - - # Set screen.width/height. - ypos = write_position.ypos - xpos = write_position.xpos - - screen.width = max(screen.width, xpos + virtual_width) - screen.height = max(screen.height, ypos + write_position.height) - - # Copy over window write positions. - self._copy_over_write_positions(screen, temp_screen, write_position) - - if temp_screen.show_cursor: - screen.show_cursor = True - - # Copy over cursor positions, if they are visible. - for window, point in temp_screen.cursor_positions.items(): - if ( - 0 <= point.x < write_position.width - and self.vertical_scroll - <= point.y - < write_position.height + self.vertical_scroll - ): - screen.cursor_positions[window] = Point( - x=point.x + xpos, y=point.y + ypos - self.vertical_scroll - ) - - # Copy over menu positions, but clip them to the visible area. - for window, point in temp_screen.menu_positions.items(): - screen.menu_positions[window] = self._clip_point_to_visible_area( - Point(x=point.x + xpos, y=point.y + ypos - self.vertical_scroll), - write_position, - ) - - # Draw scrollbar. - if show_scrollbar: - self._draw_scrollbar( - write_position, - virtual_height, - screen, - ) - - def _clip_point_to_visible_area( - self, point: Point, write_position: WritePosition - ) -> Point: - """ - Ensure that the cursor and menu positions always are always reported - """ - if point.x < write_position.xpos: - point = point._replace(x=write_position.xpos) - if point.y < write_position.ypos: - point = point._replace(y=write_position.ypos) - if point.x >= write_position.xpos + write_position.width: - point = point._replace(x=write_position.xpos + write_position.width - 1) - if point.y >= write_position.ypos + write_position.height: - point = point._replace(y=write_position.ypos + write_position.height - 1) - - return point - - def _copy_over_screen( - self, - screen: Screen, - temp_screen: Screen, - write_position: WritePosition, - virtual_width: int, - ) -> None: - """ - Copy over visible screen content and "zero width escape sequences". - """ - ypos = write_position.ypos - xpos = write_position.xpos - - for y in range(write_position.height): - temp_row = temp_screen.data_buffer[y + self.vertical_scroll] - row = screen.data_buffer[y + ypos] - temp_zero_width_escapes = temp_screen.zero_width_escapes[ - y + self.vertical_scroll - ] - zero_width_escapes = screen.zero_width_escapes[y + ypos] - - for x in range(virtual_width): - row[x + xpos] = temp_row[x] - - if x in temp_zero_width_escapes: - zero_width_escapes[x + xpos] = temp_zero_width_escapes[x] - - def _copy_over_mouse_handlers( - self, - mouse_handlers: MouseHandlers, - temp_mouse_handlers: MouseHandlers, - write_position: WritePosition, - virtual_width: int, - ) -> None: - """ - Copy over mouse handlers from virtual screen to real screen. - - Note: we take `virtual_width` because we don't want to copy over mouse - handlers that we possibly have behind the scrollbar. - """ - ypos = write_position.ypos - xpos = write_position.xpos - - # Cache mouse handlers when wrapping them. Very often the same mouse - # handler is registered for many positions. - mouse_handler_wrappers: Dict[MouseHandler, MouseHandler] = {} - - def wrap_mouse_handler(handler: MouseHandler) -> MouseHandler: - "Wrap mouse handler. Translate coordinates in `MouseEvent`." - if handler not in mouse_handler_wrappers: - - def new_handler(event: MouseEvent) -> None: - new_event = MouseEvent( - position=Point( - x=event.position.x - xpos, - y=event.position.y + self.vertical_scroll - ypos, - ), - event_type=event.event_type, - button=event.button, - modifiers=event.modifiers, - ) - handler(new_event) - - mouse_handler_wrappers[handler] = new_handler - return mouse_handler_wrappers[handler] - - # Copy handlers. - mouse_handlers_dict = mouse_handlers.mouse_handlers - temp_mouse_handlers_dict = temp_mouse_handlers.mouse_handlers - - for y in range(write_position.height): - if y in temp_mouse_handlers_dict: - temp_mouse_row = temp_mouse_handlers_dict[y + self.vertical_scroll] - mouse_row = mouse_handlers_dict[y + ypos] - for x in range(virtual_width): - if x in temp_mouse_row: - mouse_row[x + xpos] = wrap_mouse_handler(temp_mouse_row[x]) - - def _copy_over_write_positions( - self, screen: Screen, temp_screen: Screen, write_position: WritePosition - ) -> None: - """ - Copy over window write positions. - """ - ypos = write_position.ypos - xpos = write_position.xpos - - for win, write_pos in temp_screen.visible_windows_to_write_positions.items(): - screen.visible_windows_to_write_positions[win] = WritePosition( - xpos=write_pos.xpos + xpos, - ypos=write_pos.ypos + ypos - self.vertical_scroll, - # TODO: if the window is only partly visible, then truncate width/height. - # This could be important if we have nested ScrollablePanes. - height=write_pos.height, - width=write_pos.width, - ) - - def is_modal(self) -> bool: - return self.content.is_modal() - - def get_key_bindings(self) -> Optional[KeyBindingsBase]: - return self.content.get_key_bindings() - - def get_children(self) -> List["Container"]: - return [self.content] - - def _make_window_visible( - self, - visible_height: int, - virtual_height: int, - visible_win_write_pos: WritePosition, - cursor_position: Optional[Point], - ) -> None: - """ - Scroll the scrollable pane, so that this window becomes visible. - - :param visible_height: Height of this `ScrollablePane` that is rendered. - :param virtual_height: Height of the virtual, temp screen. - :param visible_win_write_pos: `WritePosition` of the nested window on the - temp screen. - :param cursor_position: The location of the cursor position of this - window on the temp screen. - """ - # Start with maximum allowed scroll range, and then reduce according to - # the focused window and cursor position. - min_scroll = 0 - max_scroll = virtual_height - visible_height - - if self.keep_cursor_visible(): - # Reduce min/max scroll according to the cursor in the focused window. - if cursor_position is not None: - offsets = self.scroll_offsets - cpos_min_scroll = ( - cursor_position.y - visible_height + 1 + offsets.bottom - ) - cpos_max_scroll = cursor_position.y - offsets.top - min_scroll = max(min_scroll, cpos_min_scroll) - max_scroll = max(0, min(max_scroll, cpos_max_scroll)) - - if self.keep_focused_window_visible(): - # Reduce min/max scroll according to focused window position. - # If the window is small enough, bot the top and bottom of the window - # should be visible. - if visible_win_write_pos.height <= visible_height: - window_min_scroll = ( - visible_win_write_pos.ypos - + visible_win_write_pos.height - - visible_height - ) - window_max_scroll = visible_win_write_pos.ypos - else: - # Window does not fit on the screen. Make sure at least the whole - # screen is occupied with this window, and nothing else is shown. - window_min_scroll = visible_win_write_pos.ypos - window_max_scroll = ( - visible_win_write_pos.ypos - + visible_win_write_pos.height - - visible_height - ) - - min_scroll = max(min_scroll, window_min_scroll) - max_scroll = min(max_scroll, window_max_scroll) - - if min_scroll > max_scroll: - min_scroll = max_scroll # Should not happen. - - # Finally, properly clip the vertical scroll. - if self.vertical_scroll > max_scroll: - self.vertical_scroll = max_scroll - if self.vertical_scroll < min_scroll: - self.vertical_scroll = min_scroll - - def _draw_scrollbar( - self, write_position: WritePosition, content_height: int, screen: Screen - ) -> None: - """ - Draw the scrollbar on the screen. - - Note: There is some code duplication with the `ScrollbarMargin` - implementation. - """ - - window_height = write_position.height - display_arrows = self.display_arrows() - - if display_arrows: - window_height -= 2 - - try: - fraction_visible = write_position.height / float(content_height) - fraction_above = self.vertical_scroll / float(content_height) - - scrollbar_height = int( - min(window_height, max(1, window_height * fraction_visible)) - ) - scrollbar_top = int(window_height * fraction_above) - except ZeroDivisionError: - return - else: - - def is_scroll_button(row: int) -> bool: - "True if we should display a button on this row." - return scrollbar_top <= row <= scrollbar_top + scrollbar_height - - xpos = write_position.xpos + write_position.width - 1 - ypos = write_position.ypos - data_buffer = screen.data_buffer - - # Up arrow. - if display_arrows: - data_buffer[ypos][xpos] = Char( - self.up_arrow_symbol, "class:scrollbar.arrow" - ) - ypos += 1 - - # Scrollbar body. - scrollbar_background = "class:scrollbar.background" - scrollbar_background_start = "class:scrollbar.background,scrollbar.start" - scrollbar_button = "class:scrollbar.button" - scrollbar_button_end = "class:scrollbar.button,scrollbar.end" - - for i in range(window_height): - style = "" - if is_scroll_button(i): - if not is_scroll_button(i + 1): - # Give the last cell a different style, because we want - # to underline this. - style = scrollbar_button_end - else: - style = scrollbar_button - else: - if is_scroll_button(i + 1): - style = scrollbar_background_start - else: - style = scrollbar_background - - data_buffer[ypos][xpos] = Char(" ", style) - ypos += 1 - - # Down arrow - if display_arrows: - data_buffer[ypos][xpos] = Char( - self.down_arrow_symbol, "class:scrollbar.arrow" - ) +from typing import Dict, List, Optional + +from prompt_toolkit.data_structures import Point +from prompt_toolkit.filters import FilterOrBool, to_filter +from prompt_toolkit.key_binding import KeyBindingsBase +from prompt_toolkit.mouse_events import MouseEvent + +from .containers import Container, ScrollOffsets +from .dimension import AnyDimension, Dimension, sum_layout_dimensions, to_dimension +from .mouse_handlers import MouseHandler, MouseHandlers +from .screen import Char, Screen, WritePosition + +__all__ = ["ScrollablePane"] + +# Never go beyond this height, because performance will degrade. +MAX_AVAILABLE_HEIGHT = 10_000 + + +class ScrollablePane(Container): + """ + Container widget that exposes a larger virtual screen to its content and + displays it in a vertical scrollbale region. + + Typically this is wrapped in a large `HSplit` container. Make sure in that + case to not specify a `height` dimension of the `HSplit`, so that it will + scale according to the content. + + .. note:: + + If you want to display a completion menu for widgets in this + `ScrollablePane`, then it's still a good practice to use a + `FloatContainer` with a `CompletionsMenu` in a `Float` at the top-level + of the layout hierarchy, rather then nesting a `FloatContainer` in this + `ScrollablePane`. (Otherwise, it's possible that the completion menu + is clipped.) + + :param content: The content container. + :param scrolloffset: Try to keep the cursor within this distance from the + top/bottom (left/right offset is not used). + :param keep_cursor_visible: When `True`, automatically scroll the pane so + that the cursor (of the focused window) is always visible. + :param keep_focused_window_visible: When `True`, automatically scroll th e + pane so that the focused window is visible, or as much visible as + possible if it doen't completely fit the screen. + :param max_available_height: Always constraint the height to this amount + for performance reasons. + :param width: When given, use this width instead of looking at the children. + :param height: When given, use this height instead of looking at the children. + :param show_scrollbar: When `True` display a scrollbar on the right. + """ + + def __init__( + self, + content: Container, + scroll_offsets: Optional[ScrollOffsets] = None, + keep_cursor_visible: FilterOrBool = True, + keep_focused_window_visible: FilterOrBool = True, + max_available_height: int = MAX_AVAILABLE_HEIGHT, + width: AnyDimension = None, + height: AnyDimension = None, + show_scrollbar: FilterOrBool = True, + display_arrows: FilterOrBool = True, + up_arrow_symbol: str = "^", + down_arrow_symbol: str = "v", + ) -> None: + self.content = content + self.scroll_offsets = scroll_offsets or ScrollOffsets(top=1, bottom=1) + self.keep_cursor_visible = to_filter(keep_cursor_visible) + self.keep_focused_window_visible = to_filter(keep_focused_window_visible) + self.max_available_height = max_available_height + self.width = width + self.height = height + self.show_scrollbar = to_filter(show_scrollbar) + self.display_arrows = to_filter(display_arrows) + self.up_arrow_symbol = up_arrow_symbol + self.down_arrow_symbol = down_arrow_symbol + + self.vertical_scroll = 0 + + def __repr__(self) -> str: + return f"ScrollablePane({self.content!r})" + + def reset(self) -> None: + self.content.reset() + + def preferred_width(self, max_available_width: int) -> Dimension: + if self.width is not None: + return to_dimension(self.width) + + # We're only scrolling vertical. So the preferred width is equal to + # that of the content. + content_width = self.content.preferred_width(max_available_width) + + # If a scrollbar needs to be displayed, add +1 to the content width. + if self.show_scrollbar(): + return sum_layout_dimensions([Dimension.exact(1), content_width]) + + return content_width + + def preferred_height(self, width: int, max_available_height: int) -> Dimension: + if self.height is not None: + return to_dimension(self.height) + + # Prefer a height large enough so that it fits all the content. If not, + # we'll make the pane scrollable. + if self.show_scrollbar(): + # If `show_scrollbar` is set. Always reserve space for the scrollbar. + width -= 1 + + dimension = self.content.preferred_height(width, self.max_available_height) + + # Only take 'preferred' into account. Min/max can be anything. + return Dimension(min=0, preferred=dimension.preferred) + + def write_to_screen( + self, + screen: Screen, + mouse_handlers: MouseHandlers, + write_position: WritePosition, + parent_style: str, + erase_bg: bool, + z_index: Optional[int], + ) -> None: + """ + Render scrollable pane content. + + This works by rendering on an off-screen canvas, and copying over the + visible region. + """ + show_scrollbar = self.show_scrollbar() + + if show_scrollbar: + virtual_width = write_position.width - 1 + else: + virtual_width = write_position.width + + # Compute preferred height again. + virtual_height = self.content.preferred_height( + virtual_width, self.max_available_height + ).preferred + + # Ensure virtual height is at least the available height. + virtual_height = max(virtual_height, write_position.height) + virtual_height = min(virtual_height, self.max_available_height) + + # First, write the content to a virtual screen, then copy over the + # visible part to the real screen. + temp_screen = Screen(default_char=Char(char=" ", style=parent_style)) + temp_write_position = WritePosition( + xpos=0, ypos=0, width=virtual_width, height=virtual_height + ) + + temp_mouse_handlers = MouseHandlers() + + self.content.write_to_screen( + temp_screen, + temp_mouse_handlers, + temp_write_position, + parent_style, + erase_bg, + z_index, + ) + temp_screen.draw_all_floats() + + # If anything in the virtual screen is focused, move vertical scroll to + from prompt_toolkit.application import get_app + + focused_window = get_app().layout.current_window + + try: + visible_win_write_pos = temp_screen.visible_windows_to_write_positions[ + focused_window + ] + except KeyError: + pass # No window focused here. Don't scroll. + else: + # Make sure this window is visible. + self._make_window_visible( + write_position.height, + virtual_height, + visible_win_write_pos, + temp_screen.cursor_positions.get(focused_window), + ) + + # Copy over virtual screen and zero width escapes to real screen. + self._copy_over_screen(screen, temp_screen, write_position, virtual_width) + + # Copy over mouse handlers. + self._copy_over_mouse_handlers( + mouse_handlers, temp_mouse_handlers, write_position, virtual_width + ) + + # Set screen.width/height. + ypos = write_position.ypos + xpos = write_position.xpos + + screen.width = max(screen.width, xpos + virtual_width) + screen.height = max(screen.height, ypos + write_position.height) + + # Copy over window write positions. + self._copy_over_write_positions(screen, temp_screen, write_position) + + if temp_screen.show_cursor: + screen.show_cursor = True + + # Copy over cursor positions, if they are visible. + for window, point in temp_screen.cursor_positions.items(): + if ( + 0 <= point.x < write_position.width + and self.vertical_scroll + <= point.y + < write_position.height + self.vertical_scroll + ): + screen.cursor_positions[window] = Point( + x=point.x + xpos, y=point.y + ypos - self.vertical_scroll + ) + + # Copy over menu positions, but clip them to the visible area. + for window, point in temp_screen.menu_positions.items(): + screen.menu_positions[window] = self._clip_point_to_visible_area( + Point(x=point.x + xpos, y=point.y + ypos - self.vertical_scroll), + write_position, + ) + + # Draw scrollbar. + if show_scrollbar: + self._draw_scrollbar( + write_position, + virtual_height, + screen, + ) + + def _clip_point_to_visible_area( + self, point: Point, write_position: WritePosition + ) -> Point: + """ + Ensure that the cursor and menu positions always are always reported + """ + if point.x < write_position.xpos: + point = point._replace(x=write_position.xpos) + if point.y < write_position.ypos: + point = point._replace(y=write_position.ypos) + if point.x >= write_position.xpos + write_position.width: + point = point._replace(x=write_position.xpos + write_position.width - 1) + if point.y >= write_position.ypos + write_position.height: + point = point._replace(y=write_position.ypos + write_position.height - 1) + + return point + + def _copy_over_screen( + self, + screen: Screen, + temp_screen: Screen, + write_position: WritePosition, + virtual_width: int, + ) -> None: + """ + Copy over visible screen content and "zero width escape sequences". + """ + ypos = write_position.ypos + xpos = write_position.xpos + + for y in range(write_position.height): + temp_row = temp_screen.data_buffer[y + self.vertical_scroll] + row = screen.data_buffer[y + ypos] + temp_zero_width_escapes = temp_screen.zero_width_escapes[ + y + self.vertical_scroll + ] + zero_width_escapes = screen.zero_width_escapes[y + ypos] + + for x in range(virtual_width): + row[x + xpos] = temp_row[x] + + if x in temp_zero_width_escapes: + zero_width_escapes[x + xpos] = temp_zero_width_escapes[x] + + def _copy_over_mouse_handlers( + self, + mouse_handlers: MouseHandlers, + temp_mouse_handlers: MouseHandlers, + write_position: WritePosition, + virtual_width: int, + ) -> None: + """ + Copy over mouse handlers from virtual screen to real screen. + + Note: we take `virtual_width` because we don't want to copy over mouse + handlers that we possibly have behind the scrollbar. + """ + ypos = write_position.ypos + xpos = write_position.xpos + + # Cache mouse handlers when wrapping them. Very often the same mouse + # handler is registered for many positions. + mouse_handler_wrappers: Dict[MouseHandler, MouseHandler] = {} + + def wrap_mouse_handler(handler: MouseHandler) -> MouseHandler: + "Wrap mouse handler. Translate coordinates in `MouseEvent`." + if handler not in mouse_handler_wrappers: + + def new_handler(event: MouseEvent) -> None: + new_event = MouseEvent( + position=Point( + x=event.position.x - xpos, + y=event.position.y + self.vertical_scroll - ypos, + ), + event_type=event.event_type, + button=event.button, + modifiers=event.modifiers, + ) + handler(new_event) + + mouse_handler_wrappers[handler] = new_handler + return mouse_handler_wrappers[handler] + + # Copy handlers. + mouse_handlers_dict = mouse_handlers.mouse_handlers + temp_mouse_handlers_dict = temp_mouse_handlers.mouse_handlers + + for y in range(write_position.height): + if y in temp_mouse_handlers_dict: + temp_mouse_row = temp_mouse_handlers_dict[y + self.vertical_scroll] + mouse_row = mouse_handlers_dict[y + ypos] + for x in range(virtual_width): + if x in temp_mouse_row: + mouse_row[x + xpos] = wrap_mouse_handler(temp_mouse_row[x]) + + def _copy_over_write_positions( + self, screen: Screen, temp_screen: Screen, write_position: WritePosition + ) -> None: + """ + Copy over window write positions. + """ + ypos = write_position.ypos + xpos = write_position.xpos + + for win, write_pos in temp_screen.visible_windows_to_write_positions.items(): + screen.visible_windows_to_write_positions[win] = WritePosition( + xpos=write_pos.xpos + xpos, + ypos=write_pos.ypos + ypos - self.vertical_scroll, + # TODO: if the window is only partly visible, then truncate width/height. + # This could be important if we have nested ScrollablePanes. + height=write_pos.height, + width=write_pos.width, + ) + + def is_modal(self) -> bool: + return self.content.is_modal() + + def get_key_bindings(self) -> Optional[KeyBindingsBase]: + return self.content.get_key_bindings() + + def get_children(self) -> List["Container"]: + return [self.content] + + def _make_window_visible( + self, + visible_height: int, + virtual_height: int, + visible_win_write_pos: WritePosition, + cursor_position: Optional[Point], + ) -> None: + """ + Scroll the scrollable pane, so that this window becomes visible. + + :param visible_height: Height of this `ScrollablePane` that is rendered. + :param virtual_height: Height of the virtual, temp screen. + :param visible_win_write_pos: `WritePosition` of the nested window on the + temp screen. + :param cursor_position: The location of the cursor position of this + window on the temp screen. + """ + # Start with maximum allowed scroll range, and then reduce according to + # the focused window and cursor position. + min_scroll = 0 + max_scroll = virtual_height - visible_height + + if self.keep_cursor_visible(): + # Reduce min/max scroll according to the cursor in the focused window. + if cursor_position is not None: + offsets = self.scroll_offsets + cpos_min_scroll = ( + cursor_position.y - visible_height + 1 + offsets.bottom + ) + cpos_max_scroll = cursor_position.y - offsets.top + min_scroll = max(min_scroll, cpos_min_scroll) + max_scroll = max(0, min(max_scroll, cpos_max_scroll)) + + if self.keep_focused_window_visible(): + # Reduce min/max scroll according to focused window position. + # If the window is small enough, bot the top and bottom of the window + # should be visible. + if visible_win_write_pos.height <= visible_height: + window_min_scroll = ( + visible_win_write_pos.ypos + + visible_win_write_pos.height + - visible_height + ) + window_max_scroll = visible_win_write_pos.ypos + else: + # Window does not fit on the screen. Make sure at least the whole + # screen is occupied with this window, and nothing else is shown. + window_min_scroll = visible_win_write_pos.ypos + window_max_scroll = ( + visible_win_write_pos.ypos + + visible_win_write_pos.height + - visible_height + ) + + min_scroll = max(min_scroll, window_min_scroll) + max_scroll = min(max_scroll, window_max_scroll) + + if min_scroll > max_scroll: + min_scroll = max_scroll # Should not happen. + + # Finally, properly clip the vertical scroll. + if self.vertical_scroll > max_scroll: + self.vertical_scroll = max_scroll + if self.vertical_scroll < min_scroll: + self.vertical_scroll = min_scroll + + def _draw_scrollbar( + self, write_position: WritePosition, content_height: int, screen: Screen + ) -> None: + """ + Draw the scrollbar on the screen. + + Note: There is some code duplication with the `ScrollbarMargin` + implementation. + """ + + window_height = write_position.height + display_arrows = self.display_arrows() + + if display_arrows: + window_height -= 2 + + try: + fraction_visible = write_position.height / float(content_height) + fraction_above = self.vertical_scroll / float(content_height) + + scrollbar_height = int( + min(window_height, max(1, window_height * fraction_visible)) + ) + scrollbar_top = int(window_height * fraction_above) + except ZeroDivisionError: + return + else: + + def is_scroll_button(row: int) -> bool: + "True if we should display a button on this row." + return scrollbar_top <= row <= scrollbar_top + scrollbar_height + + xpos = write_position.xpos + write_position.width - 1 + ypos = write_position.ypos + data_buffer = screen.data_buffer + + # Up arrow. + if display_arrows: + data_buffer[ypos][xpos] = Char( + self.up_arrow_symbol, "class:scrollbar.arrow" + ) + ypos += 1 + + # Scrollbar body. + scrollbar_background = "class:scrollbar.background" + scrollbar_background_start = "class:scrollbar.background,scrollbar.start" + scrollbar_button = "class:scrollbar.button" + scrollbar_button_end = "class:scrollbar.button,scrollbar.end" + + for i in range(window_height): + style = "" + if is_scroll_button(i): + if not is_scroll_button(i + 1): + # Give the last cell a different style, because we want + # to underline this. + style = scrollbar_button_end + else: + style = scrollbar_button + else: + if is_scroll_button(i + 1): + style = scrollbar_background_start + else: + style = scrollbar_background + + data_buffer[ypos][xpos] = Char(" ", style) + ypos += 1 + + # Down arrow + if display_arrows: + data_buffer[ypos][xpos] = Char( + self.down_arrow_symbol, "class:scrollbar.arrow" + ) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/utils.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/utils.py index 6d9eb196c8..2e0f34388b 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/utils.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/layout/utils.py @@ -1,80 +1,80 @@ -from typing import TYPE_CHECKING, Iterable, List, TypeVar, Union, cast, overload - -from prompt_toolkit.formatted_text.base import OneStyleAndTextTuple - -if TYPE_CHECKING: - from typing_extensions import SupportsIndex - -__all__ = [ - "explode_text_fragments", -] - -_T = TypeVar("_T", bound=OneStyleAndTextTuple) - - -class _ExplodedList(List[_T]): - """ - Wrapper around a list, that marks it as 'exploded'. - - As soon as items are added or the list is extended, the new items are - automatically exploded as well. - """ - - exploded = True - - def append(self, item: _T) -> None: - self.extend([item]) - - def extend(self, lst: Iterable[_T]) -> None: - super().extend(explode_text_fragments(lst)) - +from typing import TYPE_CHECKING, Iterable, List, TypeVar, Union, cast, overload + +from prompt_toolkit.formatted_text.base import OneStyleAndTextTuple + +if TYPE_CHECKING: + from typing_extensions import SupportsIndex + +__all__ = [ + "explode_text_fragments", +] + +_T = TypeVar("_T", bound=OneStyleAndTextTuple) + + +class _ExplodedList(List[_T]): + """ + Wrapper around a list, that marks it as 'exploded'. + + As soon as items are added or the list is extended, the new items are + automatically exploded as well. + """ + + exploded = True + + def append(self, item: _T) -> None: + self.extend([item]) + + def extend(self, lst: Iterable[_T]) -> None: + super().extend(explode_text_fragments(lst)) + def insert(self, index: "SupportsIndex", item: _T) -> None: - raise NotImplementedError # TODO - - # TODO: When creating a copy() or [:], return also an _ExplodedList. - - @overload - def __setitem__(self, index: "SupportsIndex", value: _T) -> None: - ... - - @overload - def __setitem__(self, index: slice, value: Iterable[_T]) -> None: - ... - - def __setitem__( - self, index: Union["SupportsIndex", slice], value: Union[_T, Iterable[_T]] - ) -> None: - """ - Ensure that when `(style_str, 'long string')` is set, the string will be - exploded. - """ - if not isinstance(index, slice): - int_index = index.__index__() - index = slice(int_index, int_index + 1) - if isinstance(value, tuple): # In case of `OneStyleAndTextTuple`. - value = cast("List[_T]", [value]) - - super().__setitem__(index, explode_text_fragments(cast("Iterable[_T]", value))) - - -def explode_text_fragments(fragments: Iterable[_T]) -> _ExplodedList[_T]: - """ - Turn a list of (style_str, text) tuples into another list where each string is - exactly one character. - - It should be fine to call this function several times. Calling this on a - list that is already exploded, is a null operation. - - :param fragments: List of (style, text) tuples. - """ - # When the fragments is already exploded, don't explode again. - if isinstance(fragments, _ExplodedList): - return fragments - - result: List[_T] = [] - - for style, string, *rest in fragments: # type: ignore - for c in string: # type: ignore - result.append((style, c, *rest)) # type: ignore - - return _ExplodedList(result) + raise NotImplementedError # TODO + + # TODO: When creating a copy() or [:], return also an _ExplodedList. + + @overload + def __setitem__(self, index: "SupportsIndex", value: _T) -> None: + ... + + @overload + def __setitem__(self, index: slice, value: Iterable[_T]) -> None: + ... + + def __setitem__( + self, index: Union["SupportsIndex", slice], value: Union[_T, Iterable[_T]] + ) -> None: + """ + Ensure that when `(style_str, 'long string')` is set, the string will be + exploded. + """ + if not isinstance(index, slice): + int_index = index.__index__() + index = slice(int_index, int_index + 1) + if isinstance(value, tuple): # In case of `OneStyleAndTextTuple`. + value = cast("List[_T]", [value]) + + super().__setitem__(index, explode_text_fragments(cast("Iterable[_T]", value))) + + +def explode_text_fragments(fragments: Iterable[_T]) -> _ExplodedList[_T]: + """ + Turn a list of (style_str, text) tuples into another list where each string is + exactly one character. + + It should be fine to call this function several times. Calling this on a + list that is already exploded, is a null operation. + + :param fragments: List of (style, text) tuples. + """ + # When the fragments is already exploded, don't explode again. + if isinstance(fragments, _ExplodedList): + return fragments + + result: List[_T] = [] + + for style, string, *rest in fragments: # type: ignore + for c in string: # type: ignore + result.append((style, c, *rest)) # type: ignore + + return _ExplodedList(result) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/lexers/__init__.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/lexers/__init__.py index c01d4d2ce0..3e875e6573 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/lexers/__init__.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/lexers/__init__.py @@ -1,18 +1,18 @@ -""" -Lexer interface and implementations. -Used for syntax highlighting. -""" -from .base import DynamicLexer, Lexer, SimpleLexer -from .pygments import PygmentsLexer, RegexSync, SyncFromStart, SyntaxSync - -__all__ = [ - # Base. - "Lexer", - "SimpleLexer", - "DynamicLexer", - # Pygments. - "PygmentsLexer", - "RegexSync", - "SyncFromStart", - "SyntaxSync", -] +""" +Lexer interface and implementations. +Used for syntax highlighting. +""" +from .base import DynamicLexer, Lexer, SimpleLexer +from .pygments import PygmentsLexer, RegexSync, SyncFromStart, SyntaxSync + +__all__ = [ + # Base. + "Lexer", + "SimpleLexer", + "DynamicLexer", + # Pygments. + "PygmentsLexer", + "RegexSync", + "SyncFromStart", + "SyntaxSync", +] diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/lexers/base.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/lexers/base.py index 813b9db935..dd9f2459af 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/lexers/base.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/lexers/base.py @@ -1,82 +1,82 @@ -""" -Base classes for prompt_toolkit lexers. -""" -from abc import ABCMeta, abstractmethod -from typing import Callable, Hashable, Optional - -from prompt_toolkit.document import Document -from prompt_toolkit.formatted_text.base import StyleAndTextTuples - -__all__ = [ - "Lexer", - "SimpleLexer", - "DynamicLexer", -] - - -class Lexer(metaclass=ABCMeta): - """ - Base class for all lexers. - """ - - @abstractmethod - def lex_document(self, document: Document) -> Callable[[int], StyleAndTextTuples]: - """ - Takes a :class:`~prompt_toolkit.document.Document` and returns a - callable that takes a line number and returns a list of - ``(style_str, text)`` tuples for that line. - - XXX: Note that in the past, this was supposed to return a list - of ``(Token, text)`` tuples, just like a Pygments lexer. - """ - - def invalidation_hash(self) -> Hashable: - """ - When this changes, `lex_document` could give a different output. - (Only used for `DynamicLexer`.) - """ - return id(self) - - -class SimpleLexer(Lexer): - """ - Lexer that doesn't do any tokenizing and returns the whole input as one - token. - - :param style: The style string for this lexer. - """ - - def __init__(self, style: str = "") -> None: - self.style = style - - def lex_document(self, document: Document) -> Callable[[int], StyleAndTextTuples]: - lines = document.lines - - def get_line(lineno: int) -> StyleAndTextTuples: - "Return the tokens for the given line." - try: - return [(self.style, lines[lineno])] - except IndexError: - return [] - - return get_line - - -class DynamicLexer(Lexer): - """ - Lexer class that can dynamically returns any Lexer. - - :param get_lexer: Callable that returns a :class:`.Lexer` instance. - """ - - def __init__(self, get_lexer: Callable[[], Optional[Lexer]]) -> None: - self.get_lexer = get_lexer - self._dummy = SimpleLexer() - - def lex_document(self, document: Document) -> Callable[[int], StyleAndTextTuples]: - lexer = self.get_lexer() or self._dummy - return lexer.lex_document(document) - - def invalidation_hash(self) -> Hashable: - lexer = self.get_lexer() or self._dummy - return id(lexer) +""" +Base classes for prompt_toolkit lexers. +""" +from abc import ABCMeta, abstractmethod +from typing import Callable, Hashable, Optional + +from prompt_toolkit.document import Document +from prompt_toolkit.formatted_text.base import StyleAndTextTuples + +__all__ = [ + "Lexer", + "SimpleLexer", + "DynamicLexer", +] + + +class Lexer(metaclass=ABCMeta): + """ + Base class for all lexers. + """ + + @abstractmethod + def lex_document(self, document: Document) -> Callable[[int], StyleAndTextTuples]: + """ + Takes a :class:`~prompt_toolkit.document.Document` and returns a + callable that takes a line number and returns a list of + ``(style_str, text)`` tuples for that line. + + XXX: Note that in the past, this was supposed to return a list + of ``(Token, text)`` tuples, just like a Pygments lexer. + """ + + def invalidation_hash(self) -> Hashable: + """ + When this changes, `lex_document` could give a different output. + (Only used for `DynamicLexer`.) + """ + return id(self) + + +class SimpleLexer(Lexer): + """ + Lexer that doesn't do any tokenizing and returns the whole input as one + token. + + :param style: The style string for this lexer. + """ + + def __init__(self, style: str = "") -> None: + self.style = style + + def lex_document(self, document: Document) -> Callable[[int], StyleAndTextTuples]: + lines = document.lines + + def get_line(lineno: int) -> StyleAndTextTuples: + "Return the tokens for the given line." + try: + return [(self.style, lines[lineno])] + except IndexError: + return [] + + return get_line + + +class DynamicLexer(Lexer): + """ + Lexer class that can dynamically returns any Lexer. + + :param get_lexer: Callable that returns a :class:`.Lexer` instance. + """ + + def __init__(self, get_lexer: Callable[[], Optional[Lexer]]) -> None: + self.get_lexer = get_lexer + self._dummy = SimpleLexer() + + def lex_document(self, document: Document) -> Callable[[int], StyleAndTextTuples]: + lexer = self.get_lexer() or self._dummy + return lexer.lex_document(document) + + def invalidation_hash(self) -> Hashable: + lexer = self.get_lexer() or self._dummy + return id(lexer) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/lexers/pygments.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/lexers/pygments.py index fd58568864..d50f8afde4 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/lexers/pygments.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/lexers/pygments.py @@ -1,335 +1,335 @@ -""" -Adaptor classes for using Pygments lexers within prompt_toolkit. - -This includes syntax synchronization code, so that we don't have to start -lexing at the beginning of a document, when displaying a very large text. -""" -import re -from abc import ABCMeta, abstractmethod -from typing import ( - TYPE_CHECKING, - Callable, - Dict, - Generator, - Iterable, - Optional, - Tuple, - Type, -) - -from prompt_toolkit.document import Document -from prompt_toolkit.filters import FilterOrBool, to_filter -from prompt_toolkit.formatted_text.base import StyleAndTextTuples -from prompt_toolkit.formatted_text.utils import split_lines -from prompt_toolkit.styles.pygments import pygments_token_to_classname - -from .base import Lexer, SimpleLexer - -if TYPE_CHECKING: - from pygments.lexer import Lexer as PygmentsLexerCls - -__all__ = [ - "PygmentsLexer", - "SyntaxSync", - "SyncFromStart", - "RegexSync", -] - - -class SyntaxSync(metaclass=ABCMeta): - """ - Syntax synchroniser. This is a tool that finds a start position for the - lexer. This is especially important when editing big documents; we don't - want to start the highlighting by running the lexer from the beginning of - the file. That is very slow when editing. - """ - - @abstractmethod - def get_sync_start_position( - self, document: Document, lineno: int - ) -> Tuple[int, int]: - """ - Return the position from where we can start lexing as a (row, column) - tuple. - - :param document: `Document` instance that contains all the lines. - :param lineno: The line that we want to highlight. (We need to return - this line, or an earlier position.) - """ - - -class SyncFromStart(SyntaxSync): - """ - Always start the syntax highlighting from the beginning. - """ - - def get_sync_start_position( - self, document: Document, lineno: int - ) -> Tuple[int, int]: - return 0, 0 - - -class RegexSync(SyntaxSync): - """ - Synchronize by starting at a line that matches the given regex pattern. - """ - - # Never go more than this amount of lines backwards for synchronisation. - # That would be too CPU intensive. - MAX_BACKWARDS = 500 - - # Start lexing at the start, if we are in the first 'n' lines and no - # synchronisation position was found. - FROM_START_IF_NO_SYNC_POS_FOUND = 100 - - def __init__(self, pattern: str) -> None: - self._compiled_pattern = re.compile(pattern) - - def get_sync_start_position( - self, document: Document, lineno: int - ) -> Tuple[int, int]: - """ - Scan backwards, and find a possible position to start. - """ - pattern = self._compiled_pattern - lines = document.lines - - # Scan upwards, until we find a point where we can start the syntax - # synchronisation. - for i in range(lineno, max(-1, lineno - self.MAX_BACKWARDS), -1): - match = pattern.match(lines[i]) - if match: - return i, match.start() - - # No synchronisation point found. If we aren't that far from the - # beginning, start at the very beginning, otherwise, just try to start - # at the current line. - if lineno < self.FROM_START_IF_NO_SYNC_POS_FOUND: - return 0, 0 - else: - return lineno, 0 - - @classmethod - def from_pygments_lexer_cls(cls, lexer_cls: "PygmentsLexerCls") -> "RegexSync": - """ - Create a :class:`.RegexSync` instance for this Pygments lexer class. - """ - patterns = { - # For Python, start highlighting at any class/def block. - "Python": r"^\s*(class|def)\s+", - "Python 3": r"^\s*(class|def)\s+", - # For HTML, start at any open/close tag definition. - "HTML": r"<[/a-zA-Z]", - # For javascript, start at a function. - "JavaScript": r"\bfunction\b" - # TODO: Add definitions for other languages. - # By default, we start at every possible line. - } - p = patterns.get(lexer_cls.name, "^") - return cls(p) - - -class _TokenCache(Dict[Tuple[str, ...], str]): - """ - Cache that converts Pygments tokens into `prompt_toolkit` style objects. - - ``Token.A.B.C`` will be converted into: - ``class:pygments,pygments.A,pygments.A.B,pygments.A.B.C`` - """ - - def __missing__(self, key: Tuple[str, ...]) -> str: - result = "class:" + pygments_token_to_classname(key) - self[key] = result - return result - - -_token_cache = _TokenCache() - - -class PygmentsLexer(Lexer): - """ - Lexer that calls a pygments lexer. - - Example:: - - from pygments.lexers.html import HtmlLexer - lexer = PygmentsLexer(HtmlLexer) - - Note: Don't forget to also load a Pygments compatible style. E.g.:: - - from prompt_toolkit.styles.from_pygments import style_from_pygments_cls - from pygments.styles import get_style_by_name - style = style_from_pygments_cls(get_style_by_name('monokai')) - - :param pygments_lexer_cls: A `Lexer` from Pygments. - :param sync_from_start: Start lexing at the start of the document. This - will always give the best results, but it will be slow for bigger - documents. (When the last part of the document is display, then the - whole document will be lexed by Pygments on every key stroke.) It is - recommended to disable this for inputs that are expected to be more - than 1,000 lines. - :param syntax_sync: `SyntaxSync` object. - """ - - # Minimum amount of lines to go backwards when starting the parser. - # This is important when the lines are retrieved in reverse order, or when - # scrolling upwards. (Due to the complexity of calculating the vertical - # scroll offset in the `Window` class, lines are not always retrieved in - # order.) - MIN_LINES_BACKWARDS = 50 - - # When a parser was started this amount of lines back, read the parser - # until we get the current line. Otherwise, start a new parser. - # (This should probably be bigger than MIN_LINES_BACKWARDS.) - REUSE_GENERATOR_MAX_DISTANCE = 100 - - def __init__( - self, - pygments_lexer_cls: Type["PygmentsLexerCls"], - sync_from_start: FilterOrBool = True, - syntax_sync: Optional[SyntaxSync] = None, - ) -> None: - - self.pygments_lexer_cls = pygments_lexer_cls - self.sync_from_start = to_filter(sync_from_start) - - # Instantiate the Pygments lexer. - self.pygments_lexer = pygments_lexer_cls( - stripnl=False, stripall=False, ensurenl=False - ) - - # Create syntax sync instance. - self.syntax_sync = syntax_sync or RegexSync.from_pygments_lexer_cls( - pygments_lexer_cls - ) - - @classmethod - def from_filename( - cls, filename: str, sync_from_start: FilterOrBool = True - ) -> "Lexer": - """ - Create a `Lexer` from a filename. - """ - # Inline imports: the Pygments dependency is optional! - from pygments.lexers import get_lexer_for_filename - from pygments.util import ClassNotFound - - try: - pygments_lexer = get_lexer_for_filename(filename) - except ClassNotFound: - return SimpleLexer() - else: - return cls(pygments_lexer.__class__, sync_from_start=sync_from_start) - - def lex_document(self, document: Document) -> Callable[[int], StyleAndTextTuples]: - """ - Create a lexer function that takes a line number and returns the list - of (style_str, text) tuples as the Pygments lexer returns for that line. - """ - LineGenerator = Generator[Tuple[int, StyleAndTextTuples], None, None] - - # Cache of already lexed lines. - cache: Dict[int, StyleAndTextTuples] = {} - - # Pygments generators that are currently lexing. - # Map lexer generator to the line number. - line_generators: Dict[LineGenerator, int] = {} - - def get_syntax_sync() -> SyntaxSync: - "The Syntax synchronisation object that we currently use." - if self.sync_from_start(): - return SyncFromStart() - else: - return self.syntax_sync - - def find_closest_generator(i: int) -> Optional[LineGenerator]: - "Return a generator close to line 'i', or None if none was found." - for generator, lineno in line_generators.items(): - if lineno < i and i - lineno < self.REUSE_GENERATOR_MAX_DISTANCE: - return generator - return None - - def create_line_generator(start_lineno: int, column: int = 0) -> LineGenerator: - """ - Create a generator that yields the lexed lines. - Each iteration it yields a (line_number, [(style_str, text), ...]) tuple. - """ - - def get_text_fragments() -> Iterable[Tuple[str, str]]: - text = "\n".join(document.lines[start_lineno:])[column:] - - # We call `get_text_fragments_unprocessed`, because `get_tokens` will - # still replace \r\n and \r by \n. (We don't want that, - # Pygments should return exactly the same amount of text, as we - # have given as input.) - for _, t, v in self.pygments_lexer.get_tokens_unprocessed(text): - # Turn Pygments `Token` object into prompt_toolkit style - # strings. - yield _token_cache[t], v - - yield from enumerate(split_lines(list(get_text_fragments())), start_lineno) - - def get_generator(i: int) -> LineGenerator: - """ - Find an already started generator that is close, or create a new one. - """ - # Find closest line generator. - generator = find_closest_generator(i) - if generator: - return generator - - # No generator found. Determine starting point for the syntax - # synchronisation first. - - # Go at least x lines back. (Make scrolling upwards more - # efficient.) - i = max(0, i - self.MIN_LINES_BACKWARDS) - - if i == 0: - row = 0 - column = 0 - else: - row, column = get_syntax_sync().get_sync_start_position(document, i) - - # Find generator close to this point, or otherwise create a new one. - generator = find_closest_generator(i) - if generator: - return generator - else: - generator = create_line_generator(row, column) - - # If the column is not 0, ignore the first line. (Which is - # incomplete. This happens when the synchronisation algorithm tells - # us to start parsing in the middle of a line.) - if column: - next(generator) - row += 1 - - line_generators[generator] = row - return generator - - def get_line(i: int) -> StyleAndTextTuples: - "Return the tokens for a given line number." - try: - return cache[i] - except KeyError: - generator = get_generator(i) - - # Exhaust the generator, until we find the requested line. - for num, line in generator: - cache[num] = line - if num == i: - line_generators[generator] = i - - # Remove the next item from the cache. - # (It could happen that it's already there, because of - # another generator that started filling these lines, - # but we want to synchronise these lines with the - # current lexer's state.) - if num + 1 in cache: - del cache[num + 1] - - return cache[num] - return [] - - return get_line +""" +Adaptor classes for using Pygments lexers within prompt_toolkit. + +This includes syntax synchronization code, so that we don't have to start +lexing at the beginning of a document, when displaying a very large text. +""" +import re +from abc import ABCMeta, abstractmethod +from typing import ( + TYPE_CHECKING, + Callable, + Dict, + Generator, + Iterable, + Optional, + Tuple, + Type, +) + +from prompt_toolkit.document import Document +from prompt_toolkit.filters import FilterOrBool, to_filter +from prompt_toolkit.formatted_text.base import StyleAndTextTuples +from prompt_toolkit.formatted_text.utils import split_lines +from prompt_toolkit.styles.pygments import pygments_token_to_classname + +from .base import Lexer, SimpleLexer + +if TYPE_CHECKING: + from pygments.lexer import Lexer as PygmentsLexerCls + +__all__ = [ + "PygmentsLexer", + "SyntaxSync", + "SyncFromStart", + "RegexSync", +] + + +class SyntaxSync(metaclass=ABCMeta): + """ + Syntax synchroniser. This is a tool that finds a start position for the + lexer. This is especially important when editing big documents; we don't + want to start the highlighting by running the lexer from the beginning of + the file. That is very slow when editing. + """ + + @abstractmethod + def get_sync_start_position( + self, document: Document, lineno: int + ) -> Tuple[int, int]: + """ + Return the position from where we can start lexing as a (row, column) + tuple. + + :param document: `Document` instance that contains all the lines. + :param lineno: The line that we want to highlight. (We need to return + this line, or an earlier position.) + """ + + +class SyncFromStart(SyntaxSync): + """ + Always start the syntax highlighting from the beginning. + """ + + def get_sync_start_position( + self, document: Document, lineno: int + ) -> Tuple[int, int]: + return 0, 0 + + +class RegexSync(SyntaxSync): + """ + Synchronize by starting at a line that matches the given regex pattern. + """ + + # Never go more than this amount of lines backwards for synchronisation. + # That would be too CPU intensive. + MAX_BACKWARDS = 500 + + # Start lexing at the start, if we are in the first 'n' lines and no + # synchronisation position was found. + FROM_START_IF_NO_SYNC_POS_FOUND = 100 + + def __init__(self, pattern: str) -> None: + self._compiled_pattern = re.compile(pattern) + + def get_sync_start_position( + self, document: Document, lineno: int + ) -> Tuple[int, int]: + """ + Scan backwards, and find a possible position to start. + """ + pattern = self._compiled_pattern + lines = document.lines + + # Scan upwards, until we find a point where we can start the syntax + # synchronisation. + for i in range(lineno, max(-1, lineno - self.MAX_BACKWARDS), -1): + match = pattern.match(lines[i]) + if match: + return i, match.start() + + # No synchronisation point found. If we aren't that far from the + # beginning, start at the very beginning, otherwise, just try to start + # at the current line. + if lineno < self.FROM_START_IF_NO_SYNC_POS_FOUND: + return 0, 0 + else: + return lineno, 0 + + @classmethod + def from_pygments_lexer_cls(cls, lexer_cls: "PygmentsLexerCls") -> "RegexSync": + """ + Create a :class:`.RegexSync` instance for this Pygments lexer class. + """ + patterns = { + # For Python, start highlighting at any class/def block. + "Python": r"^\s*(class|def)\s+", + "Python 3": r"^\s*(class|def)\s+", + # For HTML, start at any open/close tag definition. + "HTML": r"<[/a-zA-Z]", + # For javascript, start at a function. + "JavaScript": r"\bfunction\b" + # TODO: Add definitions for other languages. + # By default, we start at every possible line. + } + p = patterns.get(lexer_cls.name, "^") + return cls(p) + + +class _TokenCache(Dict[Tuple[str, ...], str]): + """ + Cache that converts Pygments tokens into `prompt_toolkit` style objects. + + ``Token.A.B.C`` will be converted into: + ``class:pygments,pygments.A,pygments.A.B,pygments.A.B.C`` + """ + + def __missing__(self, key: Tuple[str, ...]) -> str: + result = "class:" + pygments_token_to_classname(key) + self[key] = result + return result + + +_token_cache = _TokenCache() + + +class PygmentsLexer(Lexer): + """ + Lexer that calls a pygments lexer. + + Example:: + + from pygments.lexers.html import HtmlLexer + lexer = PygmentsLexer(HtmlLexer) + + Note: Don't forget to also load a Pygments compatible style. E.g.:: + + from prompt_toolkit.styles.from_pygments import style_from_pygments_cls + from pygments.styles import get_style_by_name + style = style_from_pygments_cls(get_style_by_name('monokai')) + + :param pygments_lexer_cls: A `Lexer` from Pygments. + :param sync_from_start: Start lexing at the start of the document. This + will always give the best results, but it will be slow for bigger + documents. (When the last part of the document is display, then the + whole document will be lexed by Pygments on every key stroke.) It is + recommended to disable this for inputs that are expected to be more + than 1,000 lines. + :param syntax_sync: `SyntaxSync` object. + """ + + # Minimum amount of lines to go backwards when starting the parser. + # This is important when the lines are retrieved in reverse order, or when + # scrolling upwards. (Due to the complexity of calculating the vertical + # scroll offset in the `Window` class, lines are not always retrieved in + # order.) + MIN_LINES_BACKWARDS = 50 + + # When a parser was started this amount of lines back, read the parser + # until we get the current line. Otherwise, start a new parser. + # (This should probably be bigger than MIN_LINES_BACKWARDS.) + REUSE_GENERATOR_MAX_DISTANCE = 100 + + def __init__( + self, + pygments_lexer_cls: Type["PygmentsLexerCls"], + sync_from_start: FilterOrBool = True, + syntax_sync: Optional[SyntaxSync] = None, + ) -> None: + + self.pygments_lexer_cls = pygments_lexer_cls + self.sync_from_start = to_filter(sync_from_start) + + # Instantiate the Pygments lexer. + self.pygments_lexer = pygments_lexer_cls( + stripnl=False, stripall=False, ensurenl=False + ) + + # Create syntax sync instance. + self.syntax_sync = syntax_sync or RegexSync.from_pygments_lexer_cls( + pygments_lexer_cls + ) + + @classmethod + def from_filename( + cls, filename: str, sync_from_start: FilterOrBool = True + ) -> "Lexer": + """ + Create a `Lexer` from a filename. + """ + # Inline imports: the Pygments dependency is optional! + from pygments.lexers import get_lexer_for_filename + from pygments.util import ClassNotFound + + try: + pygments_lexer = get_lexer_for_filename(filename) + except ClassNotFound: + return SimpleLexer() + else: + return cls(pygments_lexer.__class__, sync_from_start=sync_from_start) + + def lex_document(self, document: Document) -> Callable[[int], StyleAndTextTuples]: + """ + Create a lexer function that takes a line number and returns the list + of (style_str, text) tuples as the Pygments lexer returns for that line. + """ + LineGenerator = Generator[Tuple[int, StyleAndTextTuples], None, None] + + # Cache of already lexed lines. + cache: Dict[int, StyleAndTextTuples] = {} + + # Pygments generators that are currently lexing. + # Map lexer generator to the line number. + line_generators: Dict[LineGenerator, int] = {} + + def get_syntax_sync() -> SyntaxSync: + "The Syntax synchronisation object that we currently use." + if self.sync_from_start(): + return SyncFromStart() + else: + return self.syntax_sync + + def find_closest_generator(i: int) -> Optional[LineGenerator]: + "Return a generator close to line 'i', or None if none was found." + for generator, lineno in line_generators.items(): + if lineno < i and i - lineno < self.REUSE_GENERATOR_MAX_DISTANCE: + return generator + return None + + def create_line_generator(start_lineno: int, column: int = 0) -> LineGenerator: + """ + Create a generator that yields the lexed lines. + Each iteration it yields a (line_number, [(style_str, text), ...]) tuple. + """ + + def get_text_fragments() -> Iterable[Tuple[str, str]]: + text = "\n".join(document.lines[start_lineno:])[column:] + + # We call `get_text_fragments_unprocessed`, because `get_tokens` will + # still replace \r\n and \r by \n. (We don't want that, + # Pygments should return exactly the same amount of text, as we + # have given as input.) + for _, t, v in self.pygments_lexer.get_tokens_unprocessed(text): + # Turn Pygments `Token` object into prompt_toolkit style + # strings. + yield _token_cache[t], v + + yield from enumerate(split_lines(list(get_text_fragments())), start_lineno) + + def get_generator(i: int) -> LineGenerator: + """ + Find an already started generator that is close, or create a new one. + """ + # Find closest line generator. + generator = find_closest_generator(i) + if generator: + return generator + + # No generator found. Determine starting point for the syntax + # synchronisation first. + + # Go at least x lines back. (Make scrolling upwards more + # efficient.) + i = max(0, i - self.MIN_LINES_BACKWARDS) + + if i == 0: + row = 0 + column = 0 + else: + row, column = get_syntax_sync().get_sync_start_position(document, i) + + # Find generator close to this point, or otherwise create a new one. + generator = find_closest_generator(i) + if generator: + return generator + else: + generator = create_line_generator(row, column) + + # If the column is not 0, ignore the first line. (Which is + # incomplete. This happens when the synchronisation algorithm tells + # us to start parsing in the middle of a line.) + if column: + next(generator) + row += 1 + + line_generators[generator] = row + return generator + + def get_line(i: int) -> StyleAndTextTuples: + "Return the tokens for a given line number." + try: + return cache[i] + except KeyError: + generator = get_generator(i) + + # Exhaust the generator, until we find the requested line. + for num, line in generator: + cache[num] = line + if num == i: + line_generators[generator] = i + + # Remove the next item from the cache. + # (It could happen that it's already there, because of + # another generator that started filling these lines, + # but we want to synchronise these lines with the + # current lexer's state.) + if num + 1 in cache: + del cache[num + 1] + + return cache[num] + return [] + + return get_line diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/log.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/log.py index 326e70331c..36ceced49e 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/log.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/log.py @@ -1,10 +1,10 @@ -""" -Logging configuration. -""" -import logging - -__all__ = [ - "logger", -] - -logger = logging.getLogger(__package__) +""" +Logging configuration. +""" +import logging + +__all__ = [ + "logger", +] + +logger = logging.getLogger(__package__) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/mouse_events.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/mouse_events.py index 83dc9ad6ea..26f4312043 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/mouse_events.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/mouse_events.py @@ -1,88 +1,88 @@ -""" -Mouse events. - - -How it works ------------- - -The renderer has a 2 dimensional grid of mouse event handlers. -(`prompt_toolkit.layout.MouseHandlers`.) When the layout is rendered, the -`Window` class will make sure that this grid will also be filled with -callbacks. For vt100 terminals, mouse events are received through stdin, just -like any other key press. There is a handler among the key bindings that -catches these events and forwards them to such a mouse event handler. It passes -through the `Window` class where the coordinates are translated from absolute -coordinates to coordinates relative to the user control, and there -`UIControl.mouse_handler` is called. -""" -from enum import Enum -from typing import FrozenSet - -from .data_structures import Point - -__all__ = ["MouseEventType", "MouseButton", "MouseModifier", "MouseEvent"] - - -class MouseEventType(Enum): - # Mouse up: This same event type is fired for all three events: left mouse - # up, right mouse up, or middle mouse up - MOUSE_UP = "MOUSE_UP" - - # Mouse down: This implicitly refers to the left mouse down (this event is - # not fired upon pressing the middle or right mouse buttons). - MOUSE_DOWN = "MOUSE_DOWN" - - SCROLL_UP = "SCROLL_UP" - SCROLL_DOWN = "SCROLL_DOWN" - - # Triggered when the left mouse button is held down, and the mouse moves - MOUSE_MOVE = "MOUSE_MOVE" - - -class MouseButton(Enum): - LEFT = "LEFT" - MIDDLE = "MIDDLE" - RIGHT = "RIGHT" - - # When we're scrolling, or just moving the mouse and not pressing a button. - NONE = "NONE" - - # This is for when we don't know which mouse button was pressed, but we do - # know that one has been pressed during this mouse event (as opposed to - # scrolling, for example) - UNKNOWN = "UNKNOWN" - - -class MouseModifier(Enum): - SHIFT = "SHIFT" - ALT = "ALT" - CONTROL = "CONTROL" - - -class MouseEvent: - """ - Mouse event, sent to `UIControl.mouse_handler`. - - :param position: `Point` instance. - :param event_type: `MouseEventType`. - """ - - def __init__( - self, - position: Point, - event_type: MouseEventType, - button: MouseButton, - modifiers: FrozenSet[MouseModifier], - ) -> None: - self.position = position - self.event_type = event_type - self.button = button - self.modifiers = modifiers - - def __repr__(self) -> str: - return "MouseEvent(%r,%r,%r,%r)" % ( - self.position, - self.event_type, - self.button, - self.modifiers, - ) +""" +Mouse events. + + +How it works +------------ + +The renderer has a 2 dimensional grid of mouse event handlers. +(`prompt_toolkit.layout.MouseHandlers`.) When the layout is rendered, the +`Window` class will make sure that this grid will also be filled with +callbacks. For vt100 terminals, mouse events are received through stdin, just +like any other key press. There is a handler among the key bindings that +catches these events and forwards them to such a mouse event handler. It passes +through the `Window` class where the coordinates are translated from absolute +coordinates to coordinates relative to the user control, and there +`UIControl.mouse_handler` is called. +""" +from enum import Enum +from typing import FrozenSet + +from .data_structures import Point + +__all__ = ["MouseEventType", "MouseButton", "MouseModifier", "MouseEvent"] + + +class MouseEventType(Enum): + # Mouse up: This same event type is fired for all three events: left mouse + # up, right mouse up, or middle mouse up + MOUSE_UP = "MOUSE_UP" + + # Mouse down: This implicitly refers to the left mouse down (this event is + # not fired upon pressing the middle or right mouse buttons). + MOUSE_DOWN = "MOUSE_DOWN" + + SCROLL_UP = "SCROLL_UP" + SCROLL_DOWN = "SCROLL_DOWN" + + # Triggered when the left mouse button is held down, and the mouse moves + MOUSE_MOVE = "MOUSE_MOVE" + + +class MouseButton(Enum): + LEFT = "LEFT" + MIDDLE = "MIDDLE" + RIGHT = "RIGHT" + + # When we're scrolling, or just moving the mouse and not pressing a button. + NONE = "NONE" + + # This is for when we don't know which mouse button was pressed, but we do + # know that one has been pressed during this mouse event (as opposed to + # scrolling, for example) + UNKNOWN = "UNKNOWN" + + +class MouseModifier(Enum): + SHIFT = "SHIFT" + ALT = "ALT" + CONTROL = "CONTROL" + + +class MouseEvent: + """ + Mouse event, sent to `UIControl.mouse_handler`. + + :param position: `Point` instance. + :param event_type: `MouseEventType`. + """ + + def __init__( + self, + position: Point, + event_type: MouseEventType, + button: MouseButton, + modifiers: FrozenSet[MouseModifier], + ) -> None: + self.position = position + self.event_type = event_type + self.button = button + self.modifiers = modifiers + + def __repr__(self) -> str: + return "MouseEvent(%r,%r,%r,%r)" % ( + self.position, + self.event_type, + self.button, + self.modifiers, + ) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/__init__.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/__init__.py index ccfebf6ac4..7b90b476bd 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/__init__.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/__init__.py @@ -1,13 +1,13 @@ -from .base import DummyOutput, Output -from .color_depth import ColorDepth -from .defaults import create_output - -__all__ = [ - # Base. - "Output", - "DummyOutput", - # Color depth. - "ColorDepth", - # Defaults. - "create_output", -] +from .base import DummyOutput, Output +from .color_depth import ColorDepth +from .defaults import create_output + +__all__ = [ + # Base. + "Output", + "DummyOutput", + # Color depth. + "ColorDepth", + # Defaults. + "create_output", +] diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/base.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/base.py index ee2c42457e..c78677bc8b 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/base.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/base.py @@ -1,146 +1,146 @@ -""" -Interface for an output. -""" -from abc import ABCMeta, abstractmethod -from typing import Optional, TextIO - +""" +Interface for an output. +""" +from abc import ABCMeta, abstractmethod +from typing import Optional, TextIO + from prompt_toolkit.cursor_shapes import CursorShape -from prompt_toolkit.data_structures import Size -from prompt_toolkit.styles import Attrs - -from .color_depth import ColorDepth - -__all__ = [ - "Output", - "DummyOutput", -] - - -class Output(metaclass=ABCMeta): - """ - Base class defining the output interface for a - :class:`~prompt_toolkit.renderer.Renderer`. - - Actual implementations are - :class:`~prompt_toolkit.output.vt100.Vt100_Output` and - :class:`~prompt_toolkit.output.win32.Win32Output`. - """ - - stdout: Optional[TextIO] = None - - @abstractmethod - def fileno(self) -> int: - "Return the file descriptor to which we can write for the output." - - @abstractmethod - def encoding(self) -> str: - """ - Return the encoding for this output, e.g. 'utf-8'. - (This is used mainly to know which characters are supported by the - output the data, so that the UI can provide alternatives, when - required.) - """ - - @abstractmethod - def write(self, data: str) -> None: - "Write text (Terminal escape sequences will be removed/escaped.)" - - @abstractmethod - def write_raw(self, data: str) -> None: - "Write text." - - @abstractmethod - def set_title(self, title: str) -> None: - "Set terminal title." - - @abstractmethod - def clear_title(self) -> None: - "Clear title again. (or restore previous title.)" - - @abstractmethod - def flush(self) -> None: - "Write to output stream and flush." - - @abstractmethod - def erase_screen(self) -> None: - """ - Erases the screen with the background colour and moves the cursor to - home. - """ - - @abstractmethod - def enter_alternate_screen(self) -> None: - "Go to the alternate screen buffer. (For full screen applications)." - - @abstractmethod - def quit_alternate_screen(self) -> None: - "Leave the alternate screen buffer." - - @abstractmethod - def enable_mouse_support(self) -> None: - "Enable mouse." - - @abstractmethod - def disable_mouse_support(self) -> None: - "Disable mouse." - - @abstractmethod - def erase_end_of_line(self) -> None: - """ - Erases from the current cursor position to the end of the current line. - """ - - @abstractmethod - def erase_down(self) -> None: - """ - Erases the screen from the current line down to the bottom of the - screen. - """ - - @abstractmethod - def reset_attributes(self) -> None: - "Reset color and styling attributes." - - @abstractmethod - def set_attributes(self, attrs: Attrs, color_depth: ColorDepth) -> None: - "Set new color and styling attributes." - - @abstractmethod - def disable_autowrap(self) -> None: - "Disable auto line wrapping." - - @abstractmethod - def enable_autowrap(self) -> None: - "Enable auto line wrapping." - - @abstractmethod - def cursor_goto(self, row: int = 0, column: int = 0) -> None: - "Move cursor position." - - @abstractmethod - def cursor_up(self, amount: int) -> None: - "Move cursor `amount` place up." - - @abstractmethod - def cursor_down(self, amount: int) -> None: - "Move cursor `amount` place down." - - @abstractmethod - def cursor_forward(self, amount: int) -> None: - "Move cursor `amount` place forward." - - @abstractmethod - def cursor_backward(self, amount: int) -> None: - "Move cursor `amount` place backward." - - @abstractmethod - def hide_cursor(self) -> None: - "Hide cursor." - - @abstractmethod - def show_cursor(self) -> None: - "Show cursor." - +from prompt_toolkit.data_structures import Size +from prompt_toolkit.styles import Attrs + +from .color_depth import ColorDepth + +__all__ = [ + "Output", + "DummyOutput", +] + + +class Output(metaclass=ABCMeta): + """ + Base class defining the output interface for a + :class:`~prompt_toolkit.renderer.Renderer`. + + Actual implementations are + :class:`~prompt_toolkit.output.vt100.Vt100_Output` and + :class:`~prompt_toolkit.output.win32.Win32Output`. + """ + + stdout: Optional[TextIO] = None + + @abstractmethod + def fileno(self) -> int: + "Return the file descriptor to which we can write for the output." + + @abstractmethod + def encoding(self) -> str: + """ + Return the encoding for this output, e.g. 'utf-8'. + (This is used mainly to know which characters are supported by the + output the data, so that the UI can provide alternatives, when + required.) + """ + + @abstractmethod + def write(self, data: str) -> None: + "Write text (Terminal escape sequences will be removed/escaped.)" + + @abstractmethod + def write_raw(self, data: str) -> None: + "Write text." + + @abstractmethod + def set_title(self, title: str) -> None: + "Set terminal title." + + @abstractmethod + def clear_title(self) -> None: + "Clear title again. (or restore previous title.)" + + @abstractmethod + def flush(self) -> None: + "Write to output stream and flush." + + @abstractmethod + def erase_screen(self) -> None: + """ + Erases the screen with the background colour and moves the cursor to + home. + """ + + @abstractmethod + def enter_alternate_screen(self) -> None: + "Go to the alternate screen buffer. (For full screen applications)." + + @abstractmethod + def quit_alternate_screen(self) -> None: + "Leave the alternate screen buffer." + + @abstractmethod + def enable_mouse_support(self) -> None: + "Enable mouse." + + @abstractmethod + def disable_mouse_support(self) -> None: + "Disable mouse." + + @abstractmethod + def erase_end_of_line(self) -> None: + """ + Erases from the current cursor position to the end of the current line. + """ + + @abstractmethod + def erase_down(self) -> None: + """ + Erases the screen from the current line down to the bottom of the + screen. + """ + + @abstractmethod + def reset_attributes(self) -> None: + "Reset color and styling attributes." + + @abstractmethod + def set_attributes(self, attrs: Attrs, color_depth: ColorDepth) -> None: + "Set new color and styling attributes." + + @abstractmethod + def disable_autowrap(self) -> None: + "Disable auto line wrapping." + + @abstractmethod + def enable_autowrap(self) -> None: + "Enable auto line wrapping." + + @abstractmethod + def cursor_goto(self, row: int = 0, column: int = 0) -> None: + "Move cursor position." + + @abstractmethod + def cursor_up(self, amount: int) -> None: + "Move cursor `amount` place up." + + @abstractmethod + def cursor_down(self, amount: int) -> None: + "Move cursor `amount` place down." + + @abstractmethod + def cursor_forward(self, amount: int) -> None: + "Move cursor `amount` place forward." + + @abstractmethod + def cursor_backward(self, amount: int) -> None: + "Move cursor `amount` place backward." + + @abstractmethod + def hide_cursor(self) -> None: + "Hide cursor." + + @abstractmethod + def show_cursor(self) -> None: + "Show cursor." + @abstractmethod def set_cursor_shape(self, cursor_shape: CursorShape) -> None: "Set cursor shape to block, beam or underline." @@ -149,181 +149,181 @@ class Output(metaclass=ABCMeta): def reset_cursor_shape(self) -> None: "Reset cursor shape." - def ask_for_cpr(self) -> None: - """ - Asks for a cursor position report (CPR). - (VT100 only.) - """ - - @property - def responds_to_cpr(self) -> bool: - """ - `True` if the `Application` can expect to receive a CPR response after - calling `ask_for_cpr` (this will come back through the corresponding - `Input`). - - This is used to determine the amount of available rows we have below - the cursor position. In the first place, we have this so that the drop - down autocompletion menus are sized according to the available space. - - On Windows, we don't need this, there we have - `get_rows_below_cursor_position`. - """ - return False - - @abstractmethod - def get_size(self) -> Size: - "Return the size of the output window." - - def bell(self) -> None: - "Sound bell." - - def enable_bracketed_paste(self) -> None: - "For vt100 only." - - def disable_bracketed_paste(self) -> None: - "For vt100 only." - - def reset_cursor_key_mode(self) -> None: - """ - For vt100 only. - Put the terminal in normal cursor mode (instead of application mode). - - See: https://vt100.net/docs/vt100-ug/chapter3.html - """ - - def scroll_buffer_to_prompt(self) -> None: - "For Win32 only." - - def get_rows_below_cursor_position(self) -> int: - "For Windows only." - raise NotImplementedError - - @abstractmethod - def get_default_color_depth(self) -> ColorDepth: - """ - Get default color depth for this output. - - This value will be used if no color depth was explicitely passed to the - `Application`. - - .. note:: - - If the `$PROMPT_TOOLKIT_COLOR_DEPTH` environment variable has been - set, then `outputs.defaults.create_output` will pass this value to - the implementation as the default_color_depth, which is returned - here. (This is not used when the output corresponds to a - prompt_toolkit SSH/Telnet session.) - """ - - -class DummyOutput(Output): - """ - For testing. An output class that doesn't render anything. - """ - - def fileno(self) -> int: - "There is no sensible default for fileno()." - raise NotImplementedError - - def encoding(self) -> str: - return "utf-8" - - def write(self, data: str) -> None: - pass - - def write_raw(self, data: str) -> None: - pass - - def set_title(self, title: str) -> None: - pass - - def clear_title(self) -> None: - pass - - def flush(self) -> None: - pass - - def erase_screen(self) -> None: - pass - - def enter_alternate_screen(self) -> None: - pass - - def quit_alternate_screen(self) -> None: - pass - - def enable_mouse_support(self) -> None: - pass - - def disable_mouse_support(self) -> None: - pass - - def erase_end_of_line(self) -> None: - pass - - def erase_down(self) -> None: - pass - - def reset_attributes(self) -> None: - pass - - def set_attributes(self, attrs: Attrs, color_depth: ColorDepth) -> None: - pass - - def disable_autowrap(self) -> None: - pass - - def enable_autowrap(self) -> None: - pass - - def cursor_goto(self, row: int = 0, column: int = 0) -> None: - pass - - def cursor_up(self, amount: int) -> None: - pass - - def cursor_down(self, amount: int) -> None: - pass - - def cursor_forward(self, amount: int) -> None: - pass - - def cursor_backward(self, amount: int) -> None: - pass - - def hide_cursor(self) -> None: - pass - - def show_cursor(self) -> None: - pass - + def ask_for_cpr(self) -> None: + """ + Asks for a cursor position report (CPR). + (VT100 only.) + """ + + @property + def responds_to_cpr(self) -> bool: + """ + `True` if the `Application` can expect to receive a CPR response after + calling `ask_for_cpr` (this will come back through the corresponding + `Input`). + + This is used to determine the amount of available rows we have below + the cursor position. In the first place, we have this so that the drop + down autocompletion menus are sized according to the available space. + + On Windows, we don't need this, there we have + `get_rows_below_cursor_position`. + """ + return False + + @abstractmethod + def get_size(self) -> Size: + "Return the size of the output window." + + def bell(self) -> None: + "Sound bell." + + def enable_bracketed_paste(self) -> None: + "For vt100 only." + + def disable_bracketed_paste(self) -> None: + "For vt100 only." + + def reset_cursor_key_mode(self) -> None: + """ + For vt100 only. + Put the terminal in normal cursor mode (instead of application mode). + + See: https://vt100.net/docs/vt100-ug/chapter3.html + """ + + def scroll_buffer_to_prompt(self) -> None: + "For Win32 only." + + def get_rows_below_cursor_position(self) -> int: + "For Windows only." + raise NotImplementedError + + @abstractmethod + def get_default_color_depth(self) -> ColorDepth: + """ + Get default color depth for this output. + + This value will be used if no color depth was explicitely passed to the + `Application`. + + .. note:: + + If the `$PROMPT_TOOLKIT_COLOR_DEPTH` environment variable has been + set, then `outputs.defaults.create_output` will pass this value to + the implementation as the default_color_depth, which is returned + here. (This is not used when the output corresponds to a + prompt_toolkit SSH/Telnet session.) + """ + + +class DummyOutput(Output): + """ + For testing. An output class that doesn't render anything. + """ + + def fileno(self) -> int: + "There is no sensible default for fileno()." + raise NotImplementedError + + def encoding(self) -> str: + return "utf-8" + + def write(self, data: str) -> None: + pass + + def write_raw(self, data: str) -> None: + pass + + def set_title(self, title: str) -> None: + pass + + def clear_title(self) -> None: + pass + + def flush(self) -> None: + pass + + def erase_screen(self) -> None: + pass + + def enter_alternate_screen(self) -> None: + pass + + def quit_alternate_screen(self) -> None: + pass + + def enable_mouse_support(self) -> None: + pass + + def disable_mouse_support(self) -> None: + pass + + def erase_end_of_line(self) -> None: + pass + + def erase_down(self) -> None: + pass + + def reset_attributes(self) -> None: + pass + + def set_attributes(self, attrs: Attrs, color_depth: ColorDepth) -> None: + pass + + def disable_autowrap(self) -> None: + pass + + def enable_autowrap(self) -> None: + pass + + def cursor_goto(self, row: int = 0, column: int = 0) -> None: + pass + + def cursor_up(self, amount: int) -> None: + pass + + def cursor_down(self, amount: int) -> None: + pass + + def cursor_forward(self, amount: int) -> None: + pass + + def cursor_backward(self, amount: int) -> None: + pass + + def hide_cursor(self) -> None: + pass + + def show_cursor(self) -> None: + pass + def set_cursor_shape(self, cursor_shape: CursorShape) -> None: pass def reset_cursor_shape(self) -> None: pass - def ask_for_cpr(self) -> None: - pass - - def bell(self) -> None: - pass - - def enable_bracketed_paste(self) -> None: - pass - - def disable_bracketed_paste(self) -> None: - pass - - def scroll_buffer_to_prompt(self) -> None: - pass - - def get_size(self) -> Size: - return Size(rows=40, columns=80) - - def get_rows_below_cursor_position(self) -> int: - return 40 - - def get_default_color_depth(self) -> ColorDepth: - return ColorDepth.DEPTH_1_BIT + def ask_for_cpr(self) -> None: + pass + + def bell(self) -> None: + pass + + def enable_bracketed_paste(self) -> None: + pass + + def disable_bracketed_paste(self) -> None: + pass + + def scroll_buffer_to_prompt(self) -> None: + pass + + def get_size(self) -> Size: + return Size(rows=40, columns=80) + + def get_rows_below_cursor_position(self) -> int: + return 40 + + def get_default_color_depth(self) -> ColorDepth: + return ColorDepth.DEPTH_1_BIT diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/color_depth.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/color_depth.py index 9756c206af..a6166bacaf 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/color_depth.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/color_depth.py @@ -1,58 +1,58 @@ -import os -from enum import Enum -from typing import Optional - -__all__ = [ - "ColorDepth", -] - - -class ColorDepth(str, Enum): - """ - Possible color depth values for the output. - """ - - value: str - - #: One color only. - DEPTH_1_BIT = "DEPTH_1_BIT" - - #: ANSI Colors. - DEPTH_4_BIT = "DEPTH_4_BIT" - - #: The default. - DEPTH_8_BIT = "DEPTH_8_BIT" - - #: 24 bit True color. - DEPTH_24_BIT = "DEPTH_24_BIT" - - # Aliases. - MONOCHROME = DEPTH_1_BIT - ANSI_COLORS_ONLY = DEPTH_4_BIT - DEFAULT = DEPTH_8_BIT - TRUE_COLOR = DEPTH_24_BIT - - @classmethod - def from_env(cls) -> Optional["ColorDepth"]: - """ - Return the color depth if the $PROMPT_TOOLKIT_COLOR_DEPTH environment - variable has been set. - - This is a way to enforce a certain color depth in all prompt_toolkit - applications. - """ - # Check the `PROMPT_TOOLKIT_COLOR_DEPTH` environment variable. - all_values = [i.value for i in ColorDepth] - if os.environ.get("PROMPT_TOOLKIT_COLOR_DEPTH") in all_values: - return cls(os.environ["PROMPT_TOOLKIT_COLOR_DEPTH"]) - - return None - - @classmethod - def default(cls) -> "ColorDepth": - """ - Return the default color depth for the default output. - """ - from .defaults import create_output - - return create_output().get_default_color_depth() +import os +from enum import Enum +from typing import Optional + +__all__ = [ + "ColorDepth", +] + + +class ColorDepth(str, Enum): + """ + Possible color depth values for the output. + """ + + value: str + + #: One color only. + DEPTH_1_BIT = "DEPTH_1_BIT" + + #: ANSI Colors. + DEPTH_4_BIT = "DEPTH_4_BIT" + + #: The default. + DEPTH_8_BIT = "DEPTH_8_BIT" + + #: 24 bit True color. + DEPTH_24_BIT = "DEPTH_24_BIT" + + # Aliases. + MONOCHROME = DEPTH_1_BIT + ANSI_COLORS_ONLY = DEPTH_4_BIT + DEFAULT = DEPTH_8_BIT + TRUE_COLOR = DEPTH_24_BIT + + @classmethod + def from_env(cls) -> Optional["ColorDepth"]: + """ + Return the color depth if the $PROMPT_TOOLKIT_COLOR_DEPTH environment + variable has been set. + + This is a way to enforce a certain color depth in all prompt_toolkit + applications. + """ + # Check the `PROMPT_TOOLKIT_COLOR_DEPTH` environment variable. + all_values = [i.value for i in ColorDepth] + if os.environ.get("PROMPT_TOOLKIT_COLOR_DEPTH") in all_values: + return cls(os.environ["PROMPT_TOOLKIT_COLOR_DEPTH"]) + + return None + + @classmethod + def default(cls) -> "ColorDepth": + """ + Return the default color depth for the default output. + """ + from .defaults import create_output + + return create_output().get_default_color_depth() diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/conemu.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/conemu.py index f1af406b8c..ee1ac41d4d 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/conemu.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/conemu.py @@ -1,59 +1,59 @@ -from typing import Any, Optional, TextIO - -from prompt_toolkit.data_structures import Size - -from .base import Output -from .color_depth import ColorDepth -from .vt100 import Vt100_Output -from .win32 import Win32Output - -__all__ = [ - "ConEmuOutput", -] - - -class ConEmuOutput: - """ - ConEmu (Windows) output abstraction. - - ConEmu is a Windows console application, but it also supports ANSI escape - sequences. This output class is actually a proxy to both `Win32Output` and - `Vt100_Output`. It uses `Win32Output` for console sizing and scrolling, but - all cursor movements and scrolling happens through the `Vt100_Output`. - - This way, we can have 256 colors in ConEmu and Cmder. Rendering will be - even a little faster as well. - - http://conemu.github.io/ - http://gooseberrycreative.com/cmder/ - """ - - def __init__( - self, stdout: TextIO, default_color_depth: Optional[ColorDepth] = None - ) -> None: - self.win32_output = Win32Output(stdout, default_color_depth=default_color_depth) - self.vt100_output = Vt100_Output( - stdout, lambda: Size(0, 0), default_color_depth=default_color_depth - ) - - @property - def responds_to_cpr(self) -> bool: - return False # We don't need this on Windows. - - def __getattr__(self, name: str) -> Any: - if name in ( - "get_size", - "get_rows_below_cursor_position", - "enable_mouse_support", - "disable_mouse_support", - "scroll_buffer_to_prompt", - "get_win32_screen_buffer_info", - "enable_bracketed_paste", - "disable_bracketed_paste", - ): - return getattr(self.win32_output, name) - else: - return getattr(self.vt100_output, name) - - -Output.register(ConEmuOutput) +from typing import Any, Optional, TextIO + +from prompt_toolkit.data_structures import Size + +from .base import Output +from .color_depth import ColorDepth +from .vt100 import Vt100_Output +from .win32 import Win32Output + +__all__ = [ + "ConEmuOutput", +] + + +class ConEmuOutput: + """ + ConEmu (Windows) output abstraction. + + ConEmu is a Windows console application, but it also supports ANSI escape + sequences. This output class is actually a proxy to both `Win32Output` and + `Vt100_Output`. It uses `Win32Output` for console sizing and scrolling, but + all cursor movements and scrolling happens through the `Vt100_Output`. + + This way, we can have 256 colors in ConEmu and Cmder. Rendering will be + even a little faster as well. + + http://conemu.github.io/ + http://gooseberrycreative.com/cmder/ + """ + + def __init__( + self, stdout: TextIO, default_color_depth: Optional[ColorDepth] = None + ) -> None: + self.win32_output = Win32Output(stdout, default_color_depth=default_color_depth) + self.vt100_output = Vt100_Output( + stdout, lambda: Size(0, 0), default_color_depth=default_color_depth + ) + + @property + def responds_to_cpr(self) -> bool: + return False # We don't need this on Windows. + + def __getattr__(self, name: str) -> Any: + if name in ( + "get_size", + "get_rows_below_cursor_position", + "enable_mouse_support", + "disable_mouse_support", + "scroll_buffer_to_prompt", + "get_win32_screen_buffer_info", + "enable_bracketed_paste", + "disable_bracketed_paste", + ): + return getattr(self.win32_output, name) + else: + return getattr(self.vt100_output, name) + + +Output.register(ConEmuOutput) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/defaults.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/defaults.py index e2a1ef95e9..bd4bf950c4 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/defaults.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/defaults.py @@ -1,31 +1,31 @@ -import sys -from typing import Optional, TextIO, cast - -from prompt_toolkit.utils import ( - get_bell_environment_variable, - get_term_environment_variable, - is_conemu_ansi, - is_windows, -) - +import sys +from typing import Optional, TextIO, cast + +from prompt_toolkit.utils import ( + get_bell_environment_variable, + get_term_environment_variable, + is_conemu_ansi, + is_windows, +) + from .base import DummyOutput, Output -from .color_depth import ColorDepth +from .color_depth import ColorDepth from .plain_text import PlainTextOutput - -__all__ = [ - "create_output", -] - - -def create_output( + +__all__ = [ + "create_output", +] + + +def create_output( stdout: Optional[TextIO] = None, always_prefer_tty: bool = False -) -> Output: - """ - Return an :class:`~prompt_toolkit.output.Output` instance for the command - line. - - :param stdout: The stdout object - :param always_prefer_tty: When set, look for `sys.stderr` if `sys.stdout` +) -> Output: + """ + Return an :class:`~prompt_toolkit.output.Output` instance for the command + line. + + :param stdout: The stdout object + :param always_prefer_tty: When set, look for `sys.stderr` if `sys.stdout` is not a TTY. Useful if `sys.stdout` is redirected to a file, but we still want user input and output on the terminal. @@ -33,26 +33,26 @@ def create_output( it's redirected to a file), then a `PlainTextOutput` will be returned. That way, tools like `print_formatted_text` will write plain text into that file. - """ - # Consider TERM, PROMPT_TOOLKIT_BELL, and PROMPT_TOOLKIT_COLOR_DEPTH - # environment variables. Notice that PROMPT_TOOLKIT_COLOR_DEPTH value is - # the default that's used if the Application doesn't override it. - term_from_env = get_term_environment_variable() - bell_from_env = get_bell_environment_variable() - color_depth_from_env = ColorDepth.from_env() - - if stdout is None: - # By default, render to stdout. If the output is piped somewhere else, - # render to stderr. - stdout = sys.stdout - - if always_prefer_tty: - for io in [sys.stdout, sys.stderr]: + """ + # Consider TERM, PROMPT_TOOLKIT_BELL, and PROMPT_TOOLKIT_COLOR_DEPTH + # environment variables. Notice that PROMPT_TOOLKIT_COLOR_DEPTH value is + # the default that's used if the Application doesn't override it. + term_from_env = get_term_environment_variable() + bell_from_env = get_bell_environment_variable() + color_depth_from_env = ColorDepth.from_env() + + if stdout is None: + # By default, render to stdout. If the output is piped somewhere else, + # render to stderr. + stdout = sys.stdout + + if always_prefer_tty: + for io in [sys.stdout, sys.stderr]: if io is not None and io.isatty(): # (This is `None` when using `pythonw.exe` on Windows.) - stdout = io - break - + stdout = io + break + # If the output is still `None`, use a DummyOutput. # This happens for instance on Windows, when running the application under # `pythonw.exe`. In that case, there won't be a terminal Window, and @@ -60,42 +60,42 @@ def create_output( if stdout is None: return DummyOutput() - # If the patch_stdout context manager has been used, then sys.stdout is - # replaced by this proxy. For prompt_toolkit applications, we want to use - # the real stdout. - from prompt_toolkit.patch_stdout import StdoutProxy - - while isinstance(stdout, StdoutProxy): - stdout = stdout.original_stdout - - if is_windows(): - from .conemu import ConEmuOutput - from .win32 import Win32Output - from .windows10 import Windows10_Output, is_win_vt100_enabled - - if is_win_vt100_enabled(): - return cast( - Output, - Windows10_Output(stdout, default_color_depth=color_depth_from_env), - ) - if is_conemu_ansi(): - return cast( - Output, ConEmuOutput(stdout, default_color_depth=color_depth_from_env) - ) - else: - return Win32Output(stdout, default_color_depth=color_depth_from_env) - else: - from .vt100 import Vt100_Output - + # If the patch_stdout context manager has been used, then sys.stdout is + # replaced by this proxy. For prompt_toolkit applications, we want to use + # the real stdout. + from prompt_toolkit.patch_stdout import StdoutProxy + + while isinstance(stdout, StdoutProxy): + stdout = stdout.original_stdout + + if is_windows(): + from .conemu import ConEmuOutput + from .win32 import Win32Output + from .windows10 import Windows10_Output, is_win_vt100_enabled + + if is_win_vt100_enabled(): + return cast( + Output, + Windows10_Output(stdout, default_color_depth=color_depth_from_env), + ) + if is_conemu_ansi(): + return cast( + Output, ConEmuOutput(stdout, default_color_depth=color_depth_from_env) + ) + else: + return Win32Output(stdout, default_color_depth=color_depth_from_env) + else: + from .vt100 import Vt100_Output + # Stdout is not a TTY? Render as plain text. # This is mostly useful if stdout is redirected to a file, and # `print_formatted_text` is used. if not stdout.isatty(): return PlainTextOutput(stdout) - return Vt100_Output.from_pty( - stdout, - term=term_from_env, - default_color_depth=color_depth_from_env, - enable_bell=bell_from_env, - ) + return Vt100_Output.from_pty( + stdout, + term=term_from_env, + default_color_depth=color_depth_from_env, + enable_bell=bell_from_env, + ) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/vt100.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/vt100.py index fe7f85dae7..0586267286 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/vt100.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/vt100.py @@ -1,669 +1,669 @@ -""" -Output for vt100 terminals. - -A lot of thanks, regarding outputting of colors, goes to the Pygments project: -(We don't rely on Pygments anymore, because many things are very custom, and -everything has been highly optimized.) -http://pygments.org/ -""" -import io -import os -import sys -from typing import ( - Callable, - Dict, - Hashable, - Iterable, - List, - Optional, - Sequence, - Set, - TextIO, - Tuple, -) - +""" +Output for vt100 terminals. + +A lot of thanks, regarding outputting of colors, goes to the Pygments project: +(We don't rely on Pygments anymore, because many things are very custom, and +everything has been highly optimized.) +http://pygments.org/ +""" +import io +import os +import sys +from typing import ( + Callable, + Dict, + Hashable, + Iterable, + List, + Optional, + Sequence, + Set, + TextIO, + Tuple, +) + from prompt_toolkit.cursor_shapes import CursorShape -from prompt_toolkit.data_structures import Size -from prompt_toolkit.output import Output -from prompt_toolkit.styles import ANSI_COLOR_NAMES, Attrs -from prompt_toolkit.utils import is_dumb_terminal - -from .color_depth import ColorDepth +from prompt_toolkit.data_structures import Size +from prompt_toolkit.output import Output +from prompt_toolkit.styles import ANSI_COLOR_NAMES, Attrs +from prompt_toolkit.utils import is_dumb_terminal + +from .color_depth import ColorDepth from .flush_stdout import flush_stdout - -__all__ = [ - "Vt100_Output", -] - - -FG_ANSI_COLORS = { - "ansidefault": 39, - # Low intensity. - "ansiblack": 30, - "ansired": 31, - "ansigreen": 32, - "ansiyellow": 33, - "ansiblue": 34, - "ansimagenta": 35, - "ansicyan": 36, - "ansigray": 37, - # High intensity. - "ansibrightblack": 90, - "ansibrightred": 91, - "ansibrightgreen": 92, - "ansibrightyellow": 93, - "ansibrightblue": 94, - "ansibrightmagenta": 95, - "ansibrightcyan": 96, - "ansiwhite": 97, -} - -BG_ANSI_COLORS = { - "ansidefault": 49, - # Low intensity. - "ansiblack": 40, - "ansired": 41, - "ansigreen": 42, - "ansiyellow": 43, - "ansiblue": 44, - "ansimagenta": 45, - "ansicyan": 46, - "ansigray": 47, - # High intensity. - "ansibrightblack": 100, - "ansibrightred": 101, - "ansibrightgreen": 102, - "ansibrightyellow": 103, - "ansibrightblue": 104, - "ansibrightmagenta": 105, - "ansibrightcyan": 106, - "ansiwhite": 107, -} - - -ANSI_COLORS_TO_RGB = { - "ansidefault": ( - 0x00, - 0x00, - 0x00, - ), # Don't use, 'default' doesn't really have a value. - "ansiblack": (0x00, 0x00, 0x00), - "ansigray": (0xE5, 0xE5, 0xE5), - "ansibrightblack": (0x7F, 0x7F, 0x7F), - "ansiwhite": (0xFF, 0xFF, 0xFF), - # Low intensity. - "ansired": (0xCD, 0x00, 0x00), - "ansigreen": (0x00, 0xCD, 0x00), - "ansiyellow": (0xCD, 0xCD, 0x00), - "ansiblue": (0x00, 0x00, 0xCD), - "ansimagenta": (0xCD, 0x00, 0xCD), - "ansicyan": (0x00, 0xCD, 0xCD), - # High intensity. - "ansibrightred": (0xFF, 0x00, 0x00), - "ansibrightgreen": (0x00, 0xFF, 0x00), - "ansibrightyellow": (0xFF, 0xFF, 0x00), - "ansibrightblue": (0x00, 0x00, 0xFF), - "ansibrightmagenta": (0xFF, 0x00, 0xFF), - "ansibrightcyan": (0x00, 0xFF, 0xFF), -} - - -assert set(FG_ANSI_COLORS) == set(ANSI_COLOR_NAMES) -assert set(BG_ANSI_COLORS) == set(ANSI_COLOR_NAMES) -assert set(ANSI_COLORS_TO_RGB) == set(ANSI_COLOR_NAMES) - - -def _get_closest_ansi_color(r: int, g: int, b: int, exclude: Sequence[str] = ()) -> str: - """ - Find closest ANSI color. Return it by name. - - :param r: Red (Between 0 and 255.) - :param g: Green (Between 0 and 255.) - :param b: Blue (Between 0 and 255.) - :param exclude: A tuple of color names to exclude. (E.g. ``('ansired', )``.) - """ - exclude = list(exclude) - - # When we have a bit of saturation, avoid the gray-like colors, otherwise, - # too often the distance to the gray color is less. - saturation = abs(r - g) + abs(g - b) + abs(b - r) # Between 0..510 - - if saturation > 30: - exclude.extend(["ansilightgray", "ansidarkgray", "ansiwhite", "ansiblack"]) - - # Take the closest color. - # (Thanks to Pygments for this part.) - distance = 257 * 257 * 3 # "infinity" (>distance from #000000 to #ffffff) - match = "ansidefault" - - for name, (r2, g2, b2) in ANSI_COLORS_TO_RGB.items(): - if name != "ansidefault" and name not in exclude: - d = (r - r2) ** 2 + (g - g2) ** 2 + (b - b2) ** 2 - - if d < distance: - match = name - distance = d - - return match - - -_ColorCodeAndName = Tuple[int, str] - - -class _16ColorCache: - """ - Cache which maps (r, g, b) tuples to 16 ansi colors. - - :param bg: Cache for background colors, instead of foreground. - """ - - def __init__(self, bg: bool = False) -> None: - self.bg = bg - self._cache: Dict[Hashable, _ColorCodeAndName] = {} - - def get_code( - self, value: Tuple[int, int, int], exclude: Sequence[str] = () - ) -> _ColorCodeAndName: - """ - Return a (ansi_code, ansi_name) tuple. (E.g. ``(44, 'ansiblue')``.) for - a given (r,g,b) value. - """ - key: Hashable = (value, tuple(exclude)) - cache = self._cache - - if key not in cache: - cache[key] = self._get(value, exclude) - - return cache[key] - - def _get( - self, value: Tuple[int, int, int], exclude: Sequence[str] = () - ) -> _ColorCodeAndName: - - r, g, b = value - match = _get_closest_ansi_color(r, g, b, exclude=exclude) - - # Turn color name into code. - if self.bg: - code = BG_ANSI_COLORS[match] - else: - code = FG_ANSI_COLORS[match] - - return code, match - - -class _256ColorCache(Dict[Tuple[int, int, int], int]): - """ - Cache which maps (r, g, b) tuples to 256 colors. - """ - - def __init__(self) -> None: - # Build color table. - colors: List[Tuple[int, int, int]] = [] - - # colors 0..15: 16 basic colors - colors.append((0x00, 0x00, 0x00)) # 0 - colors.append((0xCD, 0x00, 0x00)) # 1 - colors.append((0x00, 0xCD, 0x00)) # 2 - colors.append((0xCD, 0xCD, 0x00)) # 3 - colors.append((0x00, 0x00, 0xEE)) # 4 - colors.append((0xCD, 0x00, 0xCD)) # 5 - colors.append((0x00, 0xCD, 0xCD)) # 6 - colors.append((0xE5, 0xE5, 0xE5)) # 7 - colors.append((0x7F, 0x7F, 0x7F)) # 8 - colors.append((0xFF, 0x00, 0x00)) # 9 - colors.append((0x00, 0xFF, 0x00)) # 10 - colors.append((0xFF, 0xFF, 0x00)) # 11 - colors.append((0x5C, 0x5C, 0xFF)) # 12 - colors.append((0xFF, 0x00, 0xFF)) # 13 - colors.append((0x00, 0xFF, 0xFF)) # 14 - colors.append((0xFF, 0xFF, 0xFF)) # 15 - - # colors 16..232: the 6x6x6 color cube - valuerange = (0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF) - - for i in range(217): - r = valuerange[(i // 36) % 6] - g = valuerange[(i // 6) % 6] - b = valuerange[i % 6] - colors.append((r, g, b)) - - # colors 233..253: grayscale - for i in range(1, 22): - v = 8 + i * 10 - colors.append((v, v, v)) - - self.colors = colors - - def __missing__(self, value: Tuple[int, int, int]) -> int: - r, g, b = value - - # Find closest color. - # (Thanks to Pygments for this!) - distance = 257 * 257 * 3 # "infinity" (>distance from #000000 to #ffffff) - match = 0 - - for i, (r2, g2, b2) in enumerate(self.colors): - if i >= 16: # XXX: We ignore the 16 ANSI colors when mapping RGB - # to the 256 colors, because these highly depend on - # the color scheme of the terminal. - d = (r - r2) ** 2 + (g - g2) ** 2 + (b - b2) ** 2 - - if d < distance: - match = i - distance = d - - # Turn color name into code. - self[value] = match - return match - - -_16_fg_colors = _16ColorCache(bg=False) -_16_bg_colors = _16ColorCache(bg=True) -_256_colors = _256ColorCache() - - -class _EscapeCodeCache(Dict[Attrs, str]): - """ - Cache for VT100 escape codes. It maps - (fgcolor, bgcolor, bold, underline, strike, reverse) tuples to VT100 - escape sequences. - - :param true_color: When True, use 24bit colors instead of 256 colors. - """ - - def __init__(self, color_depth: ColorDepth) -> None: - self.color_depth = color_depth - - def __missing__(self, attrs: Attrs) -> str: - ( - fgcolor, - bgcolor, - bold, - underline, - strike, - italic, - blink, - reverse, - hidden, - ) = attrs - parts: List[str] = [] - - parts.extend(self._colors_to_code(fgcolor or "", bgcolor or "")) - - if bold: - parts.append("1") - if italic: - parts.append("3") - if blink: - parts.append("5") - if underline: - parts.append("4") - if reverse: - parts.append("7") - if hidden: - parts.append("8") - if strike: - parts.append("9") - - if parts: - result = "\x1b[0;" + ";".join(parts) + "m" - else: - result = "\x1b[0m" - - self[attrs] = result - return result - - def _color_name_to_rgb(self, color: str) -> Tuple[int, int, int]: - "Turn 'ffffff', into (0xff, 0xff, 0xff)." - try: - rgb = int(color, 16) - except ValueError: - raise - else: - r = (rgb >> 16) & 0xFF - g = (rgb >> 8) & 0xFF - b = rgb & 0xFF - return r, g, b - - def _colors_to_code(self, fg_color: str, bg_color: str) -> Iterable[str]: - """ - Return a tuple with the vt100 values that represent this color. - """ - # When requesting ANSI colors only, and both fg/bg color were converted - # to ANSI, ensure that the foreground and background color are not the - # same. (Unless they were explicitly defined to be the same color.) - fg_ansi = "" - - def get(color: str, bg: bool) -> List[int]: - nonlocal fg_ansi - - table = BG_ANSI_COLORS if bg else FG_ANSI_COLORS - - if not color or self.color_depth == ColorDepth.DEPTH_1_BIT: - return [] - - # 16 ANSI colors. (Given by name.) - elif color in table: - return [table[color]] - - # RGB colors. (Defined as 'ffffff'.) - else: - try: - rgb = self._color_name_to_rgb(color) - except ValueError: - return [] - - # When only 16 colors are supported, use that. - if self.color_depth == ColorDepth.DEPTH_4_BIT: - if bg: # Background. - if fg_color != bg_color: - exclude = [fg_ansi] - else: - exclude = [] - code, name = _16_bg_colors.get_code(rgb, exclude=exclude) - return [code] - else: # Foreground. - code, name = _16_fg_colors.get_code(rgb) - fg_ansi = name - return [code] - - # True colors. (Only when this feature is enabled.) - elif self.color_depth == ColorDepth.DEPTH_24_BIT: - r, g, b = rgb - return [(48 if bg else 38), 2, r, g, b] - - # 256 RGB colors. - else: - return [(48 if bg else 38), 5, _256_colors[rgb]] - - result: List[int] = [] - result.extend(get(fg_color, False)) - result.extend(get(bg_color, True)) - - return map(str, result) - - -def _get_size(fileno: int) -> Tuple[int, int]: - """ - Get the size of this pseudo terminal. - - :param fileno: stdout.fileno() - :returns: A (rows, cols) tuple. - """ - size = os.get_terminal_size(fileno) - return size.lines, size.columns - - -class Vt100_Output(Output): - """ - :param get_size: A callable which returns the `Size` of the output terminal. - :param stdout: Any object with has a `write` and `flush` method + an 'encoding' property. - :param term: The terminal environment variable. (xterm, xterm-256color, linux, ...) - :param write_binary: Encode the output before writing it. If `True` (the - default), the `stdout` object is supposed to expose an `encoding` attribute. - """ - - # For the error messages. Only display "Output is not a terminal" once per - # file descriptor. - _fds_not_a_terminal: Set[int] = set() - - def __init__( - self, - stdout: TextIO, - get_size: Callable[[], Size], - term: Optional[str] = None, - write_binary: bool = True, - default_color_depth: Optional[ColorDepth] = None, - enable_bell: bool = True, - ) -> None: - - assert all(hasattr(stdout, a) for a in ("write", "flush")) - - if write_binary: - assert hasattr(stdout, "encoding") - - self._buffer: List[str] = [] - self.stdout: TextIO = stdout - self.write_binary = write_binary - self.default_color_depth = default_color_depth - self._get_size = get_size - self.term = term - self.enable_bell = enable_bell - - # Cache for escape codes. - self._escape_code_caches: Dict[ColorDepth, _EscapeCodeCache] = { - ColorDepth.DEPTH_1_BIT: _EscapeCodeCache(ColorDepth.DEPTH_1_BIT), - ColorDepth.DEPTH_4_BIT: _EscapeCodeCache(ColorDepth.DEPTH_4_BIT), - ColorDepth.DEPTH_8_BIT: _EscapeCodeCache(ColorDepth.DEPTH_8_BIT), - ColorDepth.DEPTH_24_BIT: _EscapeCodeCache(ColorDepth.DEPTH_24_BIT), - } - + +__all__ = [ + "Vt100_Output", +] + + +FG_ANSI_COLORS = { + "ansidefault": 39, + # Low intensity. + "ansiblack": 30, + "ansired": 31, + "ansigreen": 32, + "ansiyellow": 33, + "ansiblue": 34, + "ansimagenta": 35, + "ansicyan": 36, + "ansigray": 37, + # High intensity. + "ansibrightblack": 90, + "ansibrightred": 91, + "ansibrightgreen": 92, + "ansibrightyellow": 93, + "ansibrightblue": 94, + "ansibrightmagenta": 95, + "ansibrightcyan": 96, + "ansiwhite": 97, +} + +BG_ANSI_COLORS = { + "ansidefault": 49, + # Low intensity. + "ansiblack": 40, + "ansired": 41, + "ansigreen": 42, + "ansiyellow": 43, + "ansiblue": 44, + "ansimagenta": 45, + "ansicyan": 46, + "ansigray": 47, + # High intensity. + "ansibrightblack": 100, + "ansibrightred": 101, + "ansibrightgreen": 102, + "ansibrightyellow": 103, + "ansibrightblue": 104, + "ansibrightmagenta": 105, + "ansibrightcyan": 106, + "ansiwhite": 107, +} + + +ANSI_COLORS_TO_RGB = { + "ansidefault": ( + 0x00, + 0x00, + 0x00, + ), # Don't use, 'default' doesn't really have a value. + "ansiblack": (0x00, 0x00, 0x00), + "ansigray": (0xE5, 0xE5, 0xE5), + "ansibrightblack": (0x7F, 0x7F, 0x7F), + "ansiwhite": (0xFF, 0xFF, 0xFF), + # Low intensity. + "ansired": (0xCD, 0x00, 0x00), + "ansigreen": (0x00, 0xCD, 0x00), + "ansiyellow": (0xCD, 0xCD, 0x00), + "ansiblue": (0x00, 0x00, 0xCD), + "ansimagenta": (0xCD, 0x00, 0xCD), + "ansicyan": (0x00, 0xCD, 0xCD), + # High intensity. + "ansibrightred": (0xFF, 0x00, 0x00), + "ansibrightgreen": (0x00, 0xFF, 0x00), + "ansibrightyellow": (0xFF, 0xFF, 0x00), + "ansibrightblue": (0x00, 0x00, 0xFF), + "ansibrightmagenta": (0xFF, 0x00, 0xFF), + "ansibrightcyan": (0x00, 0xFF, 0xFF), +} + + +assert set(FG_ANSI_COLORS) == set(ANSI_COLOR_NAMES) +assert set(BG_ANSI_COLORS) == set(ANSI_COLOR_NAMES) +assert set(ANSI_COLORS_TO_RGB) == set(ANSI_COLOR_NAMES) + + +def _get_closest_ansi_color(r: int, g: int, b: int, exclude: Sequence[str] = ()) -> str: + """ + Find closest ANSI color. Return it by name. + + :param r: Red (Between 0 and 255.) + :param g: Green (Between 0 and 255.) + :param b: Blue (Between 0 and 255.) + :param exclude: A tuple of color names to exclude. (E.g. ``('ansired', )``.) + """ + exclude = list(exclude) + + # When we have a bit of saturation, avoid the gray-like colors, otherwise, + # too often the distance to the gray color is less. + saturation = abs(r - g) + abs(g - b) + abs(b - r) # Between 0..510 + + if saturation > 30: + exclude.extend(["ansilightgray", "ansidarkgray", "ansiwhite", "ansiblack"]) + + # Take the closest color. + # (Thanks to Pygments for this part.) + distance = 257 * 257 * 3 # "infinity" (>distance from #000000 to #ffffff) + match = "ansidefault" + + for name, (r2, g2, b2) in ANSI_COLORS_TO_RGB.items(): + if name != "ansidefault" and name not in exclude: + d = (r - r2) ** 2 + (g - g2) ** 2 + (b - b2) ** 2 + + if d < distance: + match = name + distance = d + + return match + + +_ColorCodeAndName = Tuple[int, str] + + +class _16ColorCache: + """ + Cache which maps (r, g, b) tuples to 16 ansi colors. + + :param bg: Cache for background colors, instead of foreground. + """ + + def __init__(self, bg: bool = False) -> None: + self.bg = bg + self._cache: Dict[Hashable, _ColorCodeAndName] = {} + + def get_code( + self, value: Tuple[int, int, int], exclude: Sequence[str] = () + ) -> _ColorCodeAndName: + """ + Return a (ansi_code, ansi_name) tuple. (E.g. ``(44, 'ansiblue')``.) for + a given (r,g,b) value. + """ + key: Hashable = (value, tuple(exclude)) + cache = self._cache + + if key not in cache: + cache[key] = self._get(value, exclude) + + return cache[key] + + def _get( + self, value: Tuple[int, int, int], exclude: Sequence[str] = () + ) -> _ColorCodeAndName: + + r, g, b = value + match = _get_closest_ansi_color(r, g, b, exclude=exclude) + + # Turn color name into code. + if self.bg: + code = BG_ANSI_COLORS[match] + else: + code = FG_ANSI_COLORS[match] + + return code, match + + +class _256ColorCache(Dict[Tuple[int, int, int], int]): + """ + Cache which maps (r, g, b) tuples to 256 colors. + """ + + def __init__(self) -> None: + # Build color table. + colors: List[Tuple[int, int, int]] = [] + + # colors 0..15: 16 basic colors + colors.append((0x00, 0x00, 0x00)) # 0 + colors.append((0xCD, 0x00, 0x00)) # 1 + colors.append((0x00, 0xCD, 0x00)) # 2 + colors.append((0xCD, 0xCD, 0x00)) # 3 + colors.append((0x00, 0x00, 0xEE)) # 4 + colors.append((0xCD, 0x00, 0xCD)) # 5 + colors.append((0x00, 0xCD, 0xCD)) # 6 + colors.append((0xE5, 0xE5, 0xE5)) # 7 + colors.append((0x7F, 0x7F, 0x7F)) # 8 + colors.append((0xFF, 0x00, 0x00)) # 9 + colors.append((0x00, 0xFF, 0x00)) # 10 + colors.append((0xFF, 0xFF, 0x00)) # 11 + colors.append((0x5C, 0x5C, 0xFF)) # 12 + colors.append((0xFF, 0x00, 0xFF)) # 13 + colors.append((0x00, 0xFF, 0xFF)) # 14 + colors.append((0xFF, 0xFF, 0xFF)) # 15 + + # colors 16..232: the 6x6x6 color cube + valuerange = (0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF) + + for i in range(217): + r = valuerange[(i // 36) % 6] + g = valuerange[(i // 6) % 6] + b = valuerange[i % 6] + colors.append((r, g, b)) + + # colors 233..253: grayscale + for i in range(1, 22): + v = 8 + i * 10 + colors.append((v, v, v)) + + self.colors = colors + + def __missing__(self, value: Tuple[int, int, int]) -> int: + r, g, b = value + + # Find closest color. + # (Thanks to Pygments for this!) + distance = 257 * 257 * 3 # "infinity" (>distance from #000000 to #ffffff) + match = 0 + + for i, (r2, g2, b2) in enumerate(self.colors): + if i >= 16: # XXX: We ignore the 16 ANSI colors when mapping RGB + # to the 256 colors, because these highly depend on + # the color scheme of the terminal. + d = (r - r2) ** 2 + (g - g2) ** 2 + (b - b2) ** 2 + + if d < distance: + match = i + distance = d + + # Turn color name into code. + self[value] = match + return match + + +_16_fg_colors = _16ColorCache(bg=False) +_16_bg_colors = _16ColorCache(bg=True) +_256_colors = _256ColorCache() + + +class _EscapeCodeCache(Dict[Attrs, str]): + """ + Cache for VT100 escape codes. It maps + (fgcolor, bgcolor, bold, underline, strike, reverse) tuples to VT100 + escape sequences. + + :param true_color: When True, use 24bit colors instead of 256 colors. + """ + + def __init__(self, color_depth: ColorDepth) -> None: + self.color_depth = color_depth + + def __missing__(self, attrs: Attrs) -> str: + ( + fgcolor, + bgcolor, + bold, + underline, + strike, + italic, + blink, + reverse, + hidden, + ) = attrs + parts: List[str] = [] + + parts.extend(self._colors_to_code(fgcolor or "", bgcolor or "")) + + if bold: + parts.append("1") + if italic: + parts.append("3") + if blink: + parts.append("5") + if underline: + parts.append("4") + if reverse: + parts.append("7") + if hidden: + parts.append("8") + if strike: + parts.append("9") + + if parts: + result = "\x1b[0;" + ";".join(parts) + "m" + else: + result = "\x1b[0m" + + self[attrs] = result + return result + + def _color_name_to_rgb(self, color: str) -> Tuple[int, int, int]: + "Turn 'ffffff', into (0xff, 0xff, 0xff)." + try: + rgb = int(color, 16) + except ValueError: + raise + else: + r = (rgb >> 16) & 0xFF + g = (rgb >> 8) & 0xFF + b = rgb & 0xFF + return r, g, b + + def _colors_to_code(self, fg_color: str, bg_color: str) -> Iterable[str]: + """ + Return a tuple with the vt100 values that represent this color. + """ + # When requesting ANSI colors only, and both fg/bg color were converted + # to ANSI, ensure that the foreground and background color are not the + # same. (Unless they were explicitly defined to be the same color.) + fg_ansi = "" + + def get(color: str, bg: bool) -> List[int]: + nonlocal fg_ansi + + table = BG_ANSI_COLORS if bg else FG_ANSI_COLORS + + if not color or self.color_depth == ColorDepth.DEPTH_1_BIT: + return [] + + # 16 ANSI colors. (Given by name.) + elif color in table: + return [table[color]] + + # RGB colors. (Defined as 'ffffff'.) + else: + try: + rgb = self._color_name_to_rgb(color) + except ValueError: + return [] + + # When only 16 colors are supported, use that. + if self.color_depth == ColorDepth.DEPTH_4_BIT: + if bg: # Background. + if fg_color != bg_color: + exclude = [fg_ansi] + else: + exclude = [] + code, name = _16_bg_colors.get_code(rgb, exclude=exclude) + return [code] + else: # Foreground. + code, name = _16_fg_colors.get_code(rgb) + fg_ansi = name + return [code] + + # True colors. (Only when this feature is enabled.) + elif self.color_depth == ColorDepth.DEPTH_24_BIT: + r, g, b = rgb + return [(48 if bg else 38), 2, r, g, b] + + # 256 RGB colors. + else: + return [(48 if bg else 38), 5, _256_colors[rgb]] + + result: List[int] = [] + result.extend(get(fg_color, False)) + result.extend(get(bg_color, True)) + + return map(str, result) + + +def _get_size(fileno: int) -> Tuple[int, int]: + """ + Get the size of this pseudo terminal. + + :param fileno: stdout.fileno() + :returns: A (rows, cols) tuple. + """ + size = os.get_terminal_size(fileno) + return size.lines, size.columns + + +class Vt100_Output(Output): + """ + :param get_size: A callable which returns the `Size` of the output terminal. + :param stdout: Any object with has a `write` and `flush` method + an 'encoding' property. + :param term: The terminal environment variable. (xterm, xterm-256color, linux, ...) + :param write_binary: Encode the output before writing it. If `True` (the + default), the `stdout` object is supposed to expose an `encoding` attribute. + """ + + # For the error messages. Only display "Output is not a terminal" once per + # file descriptor. + _fds_not_a_terminal: Set[int] = set() + + def __init__( + self, + stdout: TextIO, + get_size: Callable[[], Size], + term: Optional[str] = None, + write_binary: bool = True, + default_color_depth: Optional[ColorDepth] = None, + enable_bell: bool = True, + ) -> None: + + assert all(hasattr(stdout, a) for a in ("write", "flush")) + + if write_binary: + assert hasattr(stdout, "encoding") + + self._buffer: List[str] = [] + self.stdout: TextIO = stdout + self.write_binary = write_binary + self.default_color_depth = default_color_depth + self._get_size = get_size + self.term = term + self.enable_bell = enable_bell + + # Cache for escape codes. + self._escape_code_caches: Dict[ColorDepth, _EscapeCodeCache] = { + ColorDepth.DEPTH_1_BIT: _EscapeCodeCache(ColorDepth.DEPTH_1_BIT), + ColorDepth.DEPTH_4_BIT: _EscapeCodeCache(ColorDepth.DEPTH_4_BIT), + ColorDepth.DEPTH_8_BIT: _EscapeCodeCache(ColorDepth.DEPTH_8_BIT), + ColorDepth.DEPTH_24_BIT: _EscapeCodeCache(ColorDepth.DEPTH_24_BIT), + } + # Keep track of whether the cursor shape was ever changed. # (We don't restore the cursor shape if it was never changed - by # default, we don't change them.) self._cursor_shape_changed = False - @classmethod - def from_pty( - cls, - stdout: TextIO, - term: Optional[str] = None, - default_color_depth: Optional[ColorDepth] = None, - enable_bell: bool = True, - ) -> "Vt100_Output": - """ - Create an Output class from a pseudo terminal. - (This will take the dimensions by reading the pseudo - terminal attributes.) - """ - fd: Optional[int] - # Normally, this requires a real TTY device, but people instantiate - # this class often during unit tests as well. For convenience, we print - # an error message, use standard dimensions, and go on. - try: - fd = stdout.fileno() - except io.UnsupportedOperation: - fd = None - - if not stdout.isatty() and (fd is None or fd not in cls._fds_not_a_terminal): - msg = "Warning: Output is not a terminal (fd=%r).\n" - sys.stderr.write(msg % fd) - sys.stderr.flush() - if fd is not None: - cls._fds_not_a_terminal.add(fd) - - def get_size() -> Size: - # If terminal (incorrectly) reports its size as 0, pick a - # reasonable default. See - # https://github.com/ipython/ipython/issues/10071 - rows, columns = (None, None) - - # It is possible that `stdout` is no longer a TTY device at this - # point. In that case we get an `OSError` in the ioctl call in - # `get_size`. See: - # https://github.com/prompt-toolkit/python-prompt-toolkit/pull/1021 - try: - rows, columns = _get_size(stdout.fileno()) - except OSError: - pass - return Size(rows=rows or 24, columns=columns or 80) - - return cls( - stdout, - get_size, - term=term, - default_color_depth=default_color_depth, - enable_bell=enable_bell, - ) - - def get_size(self) -> Size: - return self._get_size() - - def fileno(self) -> int: - "Return file descriptor." - return self.stdout.fileno() - - def encoding(self) -> str: - "Return encoding used for stdout." - return self.stdout.encoding - - def write_raw(self, data: str) -> None: - """ - Write raw data to output. - """ - self._buffer.append(data) - - def write(self, data: str) -> None: - """ - Write text to output. - (Removes vt100 escape codes. -- used for safely writing text.) - """ - self._buffer.append(data.replace("\x1b", "?")) - - def set_title(self, title: str) -> None: - """ - Set terminal title. - """ - if self.term not in ( - "linux", - "eterm-color", - ): # Not supported by the Linux console. - self.write_raw( - "\x1b]2;%s\x07" % title.replace("\x1b", "").replace("\x07", "") - ) - - def clear_title(self) -> None: - self.set_title("") - - def erase_screen(self) -> None: - """ - Erases the screen with the background colour and moves the cursor to - home. - """ - self.write_raw("\x1b[2J") - - def enter_alternate_screen(self) -> None: - self.write_raw("\x1b[?1049h\x1b[H") - - def quit_alternate_screen(self) -> None: - self.write_raw("\x1b[?1049l") - - def enable_mouse_support(self) -> None: - self.write_raw("\x1b[?1000h") - - # Enable mouse-drag support. - self.write_raw("\x1b[?1003h") - - # Enable urxvt Mouse mode. (For terminals that understand this.) - self.write_raw("\x1b[?1015h") - - # Also enable Xterm SGR mouse mode. (For terminals that understand this.) - self.write_raw("\x1b[?1006h") - - # Note: E.g. lxterminal understands 1000h, but not the urxvt or sgr - # extensions. - - def disable_mouse_support(self) -> None: - self.write_raw("\x1b[?1000l") - self.write_raw("\x1b[?1015l") - self.write_raw("\x1b[?1006l") - self.write_raw("\x1b[?1003l") - - def erase_end_of_line(self) -> None: - """ - Erases from the current cursor position to the end of the current line. - """ - self.write_raw("\x1b[K") - - def erase_down(self) -> None: - """ - Erases the screen from the current line down to the bottom of the - screen. - """ - self.write_raw("\x1b[J") - - def reset_attributes(self) -> None: - self.write_raw("\x1b[0m") - - def set_attributes(self, attrs: Attrs, color_depth: ColorDepth) -> None: - """ - Create new style and output. - - :param attrs: `Attrs` instance. - """ - # Get current depth. - escape_code_cache = self._escape_code_caches[color_depth] - - # Write escape character. - self.write_raw(escape_code_cache[attrs]) - - def disable_autowrap(self) -> None: - self.write_raw("\x1b[?7l") - - def enable_autowrap(self) -> None: - self.write_raw("\x1b[?7h") - - def enable_bracketed_paste(self) -> None: - self.write_raw("\x1b[?2004h") - - def disable_bracketed_paste(self) -> None: - self.write_raw("\x1b[?2004l") - - def reset_cursor_key_mode(self) -> None: - """ - For vt100 only. - Put the terminal in cursor mode (instead of application mode). - """ - # Put the terminal in cursor mode. (Instead of application mode.) - self.write_raw("\x1b[?1l") - - def cursor_goto(self, row: int = 0, column: int = 0) -> None: - """ - Move cursor position. - """ - self.write_raw("\x1b[%i;%iH" % (row, column)) - - def cursor_up(self, amount: int) -> None: - if amount == 0: - pass - elif amount == 1: - self.write_raw("\x1b[A") - else: - self.write_raw("\x1b[%iA" % amount) - - def cursor_down(self, amount: int) -> None: - if amount == 0: - pass - elif amount == 1: - # Note: Not the same as '\n', '\n' can cause the window content to - # scroll. - self.write_raw("\x1b[B") - else: - self.write_raw("\x1b[%iB" % amount) - - def cursor_forward(self, amount: int) -> None: - if amount == 0: - pass - elif amount == 1: - self.write_raw("\x1b[C") - else: - self.write_raw("\x1b[%iC" % amount) - - def cursor_backward(self, amount: int) -> None: - if amount == 0: - pass - elif amount == 1: - self.write_raw("\b") # '\x1b[D' - else: - self.write_raw("\x1b[%iD" % amount) - - def hide_cursor(self) -> None: - self.write_raw("\x1b[?25l") - - def show_cursor(self) -> None: - self.write_raw("\x1b[?12l\x1b[?25h") # Stop blinking cursor and show. - + @classmethod + def from_pty( + cls, + stdout: TextIO, + term: Optional[str] = None, + default_color_depth: Optional[ColorDepth] = None, + enable_bell: bool = True, + ) -> "Vt100_Output": + """ + Create an Output class from a pseudo terminal. + (This will take the dimensions by reading the pseudo + terminal attributes.) + """ + fd: Optional[int] + # Normally, this requires a real TTY device, but people instantiate + # this class often during unit tests as well. For convenience, we print + # an error message, use standard dimensions, and go on. + try: + fd = stdout.fileno() + except io.UnsupportedOperation: + fd = None + + if not stdout.isatty() and (fd is None or fd not in cls._fds_not_a_terminal): + msg = "Warning: Output is not a terminal (fd=%r).\n" + sys.stderr.write(msg % fd) + sys.stderr.flush() + if fd is not None: + cls._fds_not_a_terminal.add(fd) + + def get_size() -> Size: + # If terminal (incorrectly) reports its size as 0, pick a + # reasonable default. See + # https://github.com/ipython/ipython/issues/10071 + rows, columns = (None, None) + + # It is possible that `stdout` is no longer a TTY device at this + # point. In that case we get an `OSError` in the ioctl call in + # `get_size`. See: + # https://github.com/prompt-toolkit/python-prompt-toolkit/pull/1021 + try: + rows, columns = _get_size(stdout.fileno()) + except OSError: + pass + return Size(rows=rows or 24, columns=columns or 80) + + return cls( + stdout, + get_size, + term=term, + default_color_depth=default_color_depth, + enable_bell=enable_bell, + ) + + def get_size(self) -> Size: + return self._get_size() + + def fileno(self) -> int: + "Return file descriptor." + return self.stdout.fileno() + + def encoding(self) -> str: + "Return encoding used for stdout." + return self.stdout.encoding + + def write_raw(self, data: str) -> None: + """ + Write raw data to output. + """ + self._buffer.append(data) + + def write(self, data: str) -> None: + """ + Write text to output. + (Removes vt100 escape codes. -- used for safely writing text.) + """ + self._buffer.append(data.replace("\x1b", "?")) + + def set_title(self, title: str) -> None: + """ + Set terminal title. + """ + if self.term not in ( + "linux", + "eterm-color", + ): # Not supported by the Linux console. + self.write_raw( + "\x1b]2;%s\x07" % title.replace("\x1b", "").replace("\x07", "") + ) + + def clear_title(self) -> None: + self.set_title("") + + def erase_screen(self) -> None: + """ + Erases the screen with the background colour and moves the cursor to + home. + """ + self.write_raw("\x1b[2J") + + def enter_alternate_screen(self) -> None: + self.write_raw("\x1b[?1049h\x1b[H") + + def quit_alternate_screen(self) -> None: + self.write_raw("\x1b[?1049l") + + def enable_mouse_support(self) -> None: + self.write_raw("\x1b[?1000h") + + # Enable mouse-drag support. + self.write_raw("\x1b[?1003h") + + # Enable urxvt Mouse mode. (For terminals that understand this.) + self.write_raw("\x1b[?1015h") + + # Also enable Xterm SGR mouse mode. (For terminals that understand this.) + self.write_raw("\x1b[?1006h") + + # Note: E.g. lxterminal understands 1000h, but not the urxvt or sgr + # extensions. + + def disable_mouse_support(self) -> None: + self.write_raw("\x1b[?1000l") + self.write_raw("\x1b[?1015l") + self.write_raw("\x1b[?1006l") + self.write_raw("\x1b[?1003l") + + def erase_end_of_line(self) -> None: + """ + Erases from the current cursor position to the end of the current line. + """ + self.write_raw("\x1b[K") + + def erase_down(self) -> None: + """ + Erases the screen from the current line down to the bottom of the + screen. + """ + self.write_raw("\x1b[J") + + def reset_attributes(self) -> None: + self.write_raw("\x1b[0m") + + def set_attributes(self, attrs: Attrs, color_depth: ColorDepth) -> None: + """ + Create new style and output. + + :param attrs: `Attrs` instance. + """ + # Get current depth. + escape_code_cache = self._escape_code_caches[color_depth] + + # Write escape character. + self.write_raw(escape_code_cache[attrs]) + + def disable_autowrap(self) -> None: + self.write_raw("\x1b[?7l") + + def enable_autowrap(self) -> None: + self.write_raw("\x1b[?7h") + + def enable_bracketed_paste(self) -> None: + self.write_raw("\x1b[?2004h") + + def disable_bracketed_paste(self) -> None: + self.write_raw("\x1b[?2004l") + + def reset_cursor_key_mode(self) -> None: + """ + For vt100 only. + Put the terminal in cursor mode (instead of application mode). + """ + # Put the terminal in cursor mode. (Instead of application mode.) + self.write_raw("\x1b[?1l") + + def cursor_goto(self, row: int = 0, column: int = 0) -> None: + """ + Move cursor position. + """ + self.write_raw("\x1b[%i;%iH" % (row, column)) + + def cursor_up(self, amount: int) -> None: + if amount == 0: + pass + elif amount == 1: + self.write_raw("\x1b[A") + else: + self.write_raw("\x1b[%iA" % amount) + + def cursor_down(self, amount: int) -> None: + if amount == 0: + pass + elif amount == 1: + # Note: Not the same as '\n', '\n' can cause the window content to + # scroll. + self.write_raw("\x1b[B") + else: + self.write_raw("\x1b[%iB" % amount) + + def cursor_forward(self, amount: int) -> None: + if amount == 0: + pass + elif amount == 1: + self.write_raw("\x1b[C") + else: + self.write_raw("\x1b[%iC" % amount) + + def cursor_backward(self, amount: int) -> None: + if amount == 0: + pass + elif amount == 1: + self.write_raw("\b") # '\x1b[D' + else: + self.write_raw("\x1b[%iD" % amount) + + def hide_cursor(self) -> None: + self.write_raw("\x1b[?25l") + + def show_cursor(self) -> None: + self.write_raw("\x1b[?12l\x1b[?25h") # Stop blinking cursor and show. + def set_cursor_shape(self, cursor_shape: CursorShape) -> None: if cursor_shape == CursorShape._NEVER_CHANGE: return @@ -689,65 +689,65 @@ class Vt100_Output(Output): # Reset cursor shape. self.write_raw("\x1b[0 q") - def flush(self) -> None: - """ - Write to output stream and flush. - """ - if not self._buffer: - return - - data = "".join(self._buffer) - self._buffer = [] - + def flush(self) -> None: + """ + Write to output stream and flush. + """ + if not self._buffer: + return + + data = "".join(self._buffer) + self._buffer = [] + flush_stdout(self.stdout, data, write_binary=self.write_binary) - - def ask_for_cpr(self) -> None: - """ - Asks for a cursor position report (CPR). - """ - self.write_raw("\x1b[6n") - self.flush() - - @property - def responds_to_cpr(self) -> bool: - # When the input is a tty, we assume that CPR is supported. - # It's not when the input is piped from Pexpect. - if os.environ.get("PROMPT_TOOLKIT_NO_CPR", "") == "1": - return False - - if is_dumb_terminal(self.term): - return False - try: - return self.stdout.isatty() - except ValueError: - return False # ValueError: I/O operation on closed file - - def bell(self) -> None: - "Sound bell." - if self.enable_bell: - self.write_raw("\a") - self.flush() - - def get_default_color_depth(self) -> ColorDepth: - """ - Return the default color depth for a vt100 terminal, according to the - our term value. - - We prefer 256 colors almost always, because this is what most terminals - support these days, and is a good default. - """ - if self.default_color_depth is not None: - return self.default_color_depth - - term = self.term - - if term is None: - return ColorDepth.DEFAULT - - if is_dumb_terminal(term): - return ColorDepth.DEPTH_1_BIT - - if term in ("linux", "eterm-color"): - return ColorDepth.DEPTH_4_BIT - - return ColorDepth.DEFAULT + + def ask_for_cpr(self) -> None: + """ + Asks for a cursor position report (CPR). + """ + self.write_raw("\x1b[6n") + self.flush() + + @property + def responds_to_cpr(self) -> bool: + # When the input is a tty, we assume that CPR is supported. + # It's not when the input is piped from Pexpect. + if os.environ.get("PROMPT_TOOLKIT_NO_CPR", "") == "1": + return False + + if is_dumb_terminal(self.term): + return False + try: + return self.stdout.isatty() + except ValueError: + return False # ValueError: I/O operation on closed file + + def bell(self) -> None: + "Sound bell." + if self.enable_bell: + self.write_raw("\a") + self.flush() + + def get_default_color_depth(self) -> ColorDepth: + """ + Return the default color depth for a vt100 terminal, according to the + our term value. + + We prefer 256 colors almost always, because this is what most terminals + support these days, and is a good default. + """ + if self.default_color_depth is not None: + return self.default_color_depth + + term = self.term + + if term is None: + return ColorDepth.DEFAULT + + if is_dumb_terminal(term): + return ColorDepth.DEPTH_1_BIT + + if term in ("linux", "eterm-color"): + return ColorDepth.DEPTH_4_BIT + + return ColorDepth.DEFAULT diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/win32.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/win32.py index ca55bfdc6f..abfd61774b 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/win32.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/win32.py @@ -1,685 +1,685 @@ -import os +import os from ctypes import ArgumentError, byref, c_char, c_long, c_uint, c_ulong, pointer - -from ..utils import SPHINX_AUTODOC_RUNNING - -# Do not import win32-specific stuff when generating documentation. -# Otherwise RTD would be unable to generate docs for this module. -if not SPHINX_AUTODOC_RUNNING: - from ctypes import windll - -from ctypes.wintypes import DWORD, HANDLE -from typing import Callable, Dict, List, Optional, TextIO, Tuple, Type, TypeVar, Union - + +from ..utils import SPHINX_AUTODOC_RUNNING + +# Do not import win32-specific stuff when generating documentation. +# Otherwise RTD would be unable to generate docs for this module. +if not SPHINX_AUTODOC_RUNNING: + from ctypes import windll + +from ctypes.wintypes import DWORD, HANDLE +from typing import Callable, Dict, List, Optional, TextIO, Tuple, Type, TypeVar, Union + from prompt_toolkit.cursor_shapes import CursorShape -from prompt_toolkit.data_structures import Size -from prompt_toolkit.styles import ANSI_COLOR_NAMES, Attrs -from prompt_toolkit.utils import get_cwidth -from prompt_toolkit.win32_types import ( - CONSOLE_SCREEN_BUFFER_INFO, - COORD, - SMALL_RECT, - STD_INPUT_HANDLE, - STD_OUTPUT_HANDLE, -) - -from .base import Output -from .color_depth import ColorDepth - -__all__ = [ - "Win32Output", -] - - -def _coord_byval(coord: COORD) -> c_long: - """ - Turns a COORD object into a c_long. - This will cause it to be passed by value instead of by reference. (That is what I think at least.) - - When running ``ptipython`` is run (only with IPython), we often got the following error:: - - Error in 'SetConsoleCursorPosition'. - ArgumentError("argument 2: <class 'TypeError'>: wrong type",) - argument 2: <class 'TypeError'>: wrong type - - It was solved by turning ``COORD`` parameters into a ``c_long`` like this. - - More info: http://msdn.microsoft.com/en-us/library/windows/desktop/ms686025(v=vs.85).aspx - """ - return c_long(coord.Y * 0x10000 | coord.X & 0xFFFF) - - -#: If True: write the output of the renderer also to the following file. This -#: is very useful for debugging. (e.g.: to see that we don't write more bytes -#: than required.) -_DEBUG_RENDER_OUTPUT = False -_DEBUG_RENDER_OUTPUT_FILENAME = r"prompt-toolkit-windows-output.log" - - -class NoConsoleScreenBufferError(Exception): - """ - Raised when the application is not running inside a Windows Console, but - the user tries to instantiate Win32Output. - """ - - def __init__(self) -> None: - # Are we running in 'xterm' on Windows, like git-bash for instance? - xterm = "xterm" in os.environ.get("TERM", "") - - if xterm: - message = ( - "Found %s, while expecting a Windows console. " - 'Maybe try to run this program using "winpty" ' - "or run it in cmd.exe instead. Or otherwise, " - "in case of Cygwin, use the Python executable " - "that is compiled for Cygwin." % os.environ["TERM"] - ) - else: - message = "No Windows console found. Are you running cmd.exe?" - super().__init__(message) - - -_T = TypeVar("_T") - - -class Win32Output(Output): - """ - I/O abstraction for rendering to Windows consoles. - (cmd.exe and similar.) - """ - - def __init__( - self, - stdout: TextIO, - use_complete_width: bool = False, - default_color_depth: Optional[ColorDepth] = None, - ) -> None: - self.use_complete_width = use_complete_width - self.default_color_depth = default_color_depth - - self._buffer: List[str] = [] - self.stdout: TextIO = stdout - self.hconsole = HANDLE(windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE)) - - self._in_alternate_screen = False - self._hidden = False - - self.color_lookup_table = ColorLookupTable() - - # Remember the default console colors. - info = self.get_win32_screen_buffer_info() - self.default_attrs = info.wAttributes if info else 15 - - if _DEBUG_RENDER_OUTPUT: - self.LOG = open(_DEBUG_RENDER_OUTPUT_FILENAME, "ab") - - def fileno(self) -> int: - "Return file descriptor." - return self.stdout.fileno() - - def encoding(self) -> str: - "Return encoding used for stdout." - return self.stdout.encoding - - def write(self, data: str) -> None: - if self._hidden: - data = " " * get_cwidth(data) - - self._buffer.append(data) - - def write_raw(self, data: str) -> None: - "For win32, there is no difference between write and write_raw." - self.write(data) - - def get_size(self) -> Size: - info = self.get_win32_screen_buffer_info() - - # We take the width of the *visible* region as the size. Not the width - # of the complete screen buffer. (Unless use_complete_width has been - # set.) - if self.use_complete_width: - width = info.dwSize.X - else: - width = info.srWindow.Right - info.srWindow.Left - - height = info.srWindow.Bottom - info.srWindow.Top + 1 - - # We avoid the right margin, windows will wrap otherwise. - maxwidth = info.dwSize.X - 1 - width = min(maxwidth, width) - - # Create `Size` object. - return Size(rows=height, columns=width) - - def _winapi(self, func: Callable[..., _T], *a: object, **kw: object) -> _T: - """ - Flush and call win API function. - """ - self.flush() - - if _DEBUG_RENDER_OUTPUT: - self.LOG.write(("%r" % func.__name__).encode("utf-8") + b"\n") - self.LOG.write( - b" " + ", ".join(["%r" % i for i in a]).encode("utf-8") + b"\n" - ) - self.LOG.write( - b" " - + ", ".join(["%r" % type(i) for i in a]).encode("utf-8") - + b"\n" - ) - self.LOG.flush() - - try: - return func(*a, **kw) - except ArgumentError as e: - if _DEBUG_RENDER_OUTPUT: - self.LOG.write( - (" Error in %r %r %s\n" % (func.__name__, e, e)).encode("utf-8") - ) - - raise - - def get_win32_screen_buffer_info(self) -> CONSOLE_SCREEN_BUFFER_INFO: - """ - Return Screen buffer info. - """ - # NOTE: We don't call the `GetConsoleScreenBufferInfo` API through - # `self._winapi`. Doing so causes Python to crash on certain 64bit - # Python versions. (Reproduced with 64bit Python 2.7.6, on Windows - # 10). It is not clear why. Possibly, it has to do with passing - # these objects as an argument, or through *args. - - # The Python documentation contains the following - possibly related - warning: - # ctypes does not support passing unions or structures with - # bit-fields to functions by value. While this may work on 32-bit - # x86, it's not guaranteed by the library to work in the general - # case. Unions and structures with bit-fields should always be - # passed to functions by pointer. - - # Also see: - # - https://github.com/ipython/ipython/issues/10070 - # - https://github.com/jonathanslenders/python-prompt-toolkit/issues/406 - # - https://github.com/jonathanslenders/python-prompt-toolkit/issues/86 - - self.flush() - sbinfo = CONSOLE_SCREEN_BUFFER_INFO() - success = windll.kernel32.GetConsoleScreenBufferInfo( - self.hconsole, byref(sbinfo) - ) - - # success = self._winapi(windll.kernel32.GetConsoleScreenBufferInfo, - # self.hconsole, byref(sbinfo)) - - if success: - return sbinfo - else: - raise NoConsoleScreenBufferError - - def set_title(self, title: str) -> None: - """ - Set terminal title. - """ - self._winapi(windll.kernel32.SetConsoleTitleW, title) - - def clear_title(self) -> None: - self._winapi(windll.kernel32.SetConsoleTitleW, "") - - def erase_screen(self) -> None: - start = COORD(0, 0) - sbinfo = self.get_win32_screen_buffer_info() - length = sbinfo.dwSize.X * sbinfo.dwSize.Y - - self.cursor_goto(row=0, column=0) - self._erase(start, length) - - def erase_down(self) -> None: - sbinfo = self.get_win32_screen_buffer_info() - size = sbinfo.dwSize - - start = sbinfo.dwCursorPosition - length = (size.X - size.X) + size.X * (size.Y - sbinfo.dwCursorPosition.Y) - - self._erase(start, length) - - def erase_end_of_line(self) -> None: - """""" - sbinfo = self.get_win32_screen_buffer_info() - start = sbinfo.dwCursorPosition - length = sbinfo.dwSize.X - sbinfo.dwCursorPosition.X - - self._erase(start, length) - - def _erase(self, start: COORD, length: int) -> None: - chars_written = c_ulong() - - self._winapi( - windll.kernel32.FillConsoleOutputCharacterA, - self.hconsole, - c_char(b" "), - DWORD(length), - _coord_byval(start), - byref(chars_written), - ) - - # Reset attributes. - sbinfo = self.get_win32_screen_buffer_info() - self._winapi( - windll.kernel32.FillConsoleOutputAttribute, - self.hconsole, - sbinfo.wAttributes, - length, - _coord_byval(start), - byref(chars_written), - ) - - def reset_attributes(self) -> None: - "Reset the console foreground/background color." - self._winapi( - windll.kernel32.SetConsoleTextAttribute, self.hconsole, self.default_attrs - ) - self._hidden = False - - def set_attributes(self, attrs: Attrs, color_depth: ColorDepth) -> None: - ( - fgcolor, - bgcolor, - bold, - underline, - strike, - italic, - blink, - reverse, - hidden, - ) = attrs - self._hidden = bool(hidden) - - # Start from the default attributes. - win_attrs: int = self.default_attrs - - if color_depth != ColorDepth.DEPTH_1_BIT: - # Override the last four bits: foreground color. - if fgcolor: - win_attrs = win_attrs & ~0xF - win_attrs |= self.color_lookup_table.lookup_fg_color(fgcolor) - - # Override the next four bits: background color. - if bgcolor: - win_attrs = win_attrs & ~0xF0 - win_attrs |= self.color_lookup_table.lookup_bg_color(bgcolor) - - # Reverse: swap these four bits groups. - if reverse: - win_attrs = ( - (win_attrs & ~0xFF) - | ((win_attrs & 0xF) << 4) - | ((win_attrs & 0xF0) >> 4) - ) - - self._winapi(windll.kernel32.SetConsoleTextAttribute, self.hconsole, win_attrs) - - def disable_autowrap(self) -> None: - # Not supported by Windows. - pass - - def enable_autowrap(self) -> None: - # Not supported by Windows. - pass - - def cursor_goto(self, row: int = 0, column: int = 0) -> None: - pos = COORD(X=column, Y=row) - self._winapi( - windll.kernel32.SetConsoleCursorPosition, self.hconsole, _coord_byval(pos) - ) - - def cursor_up(self, amount: int) -> None: - sr = self.get_win32_screen_buffer_info().dwCursorPosition - pos = COORD(X=sr.X, Y=sr.Y - amount) - self._winapi( - windll.kernel32.SetConsoleCursorPosition, self.hconsole, _coord_byval(pos) - ) - - def cursor_down(self, amount: int) -> None: - self.cursor_up(-amount) - - def cursor_forward(self, amount: int) -> None: - sr = self.get_win32_screen_buffer_info().dwCursorPosition - # assert sr.X + amount >= 0, 'Negative cursor position: x=%r amount=%r' % (sr.X, amount) - - pos = COORD(X=max(0, sr.X + amount), Y=sr.Y) - self._winapi( - windll.kernel32.SetConsoleCursorPosition, self.hconsole, _coord_byval(pos) - ) - - def cursor_backward(self, amount: int) -> None: - self.cursor_forward(-amount) - - def flush(self) -> None: - """ - Write to output stream and flush. - """ - if not self._buffer: - # Only flush stdout buffer. (It could be that Python still has - # something in its buffer. -- We want to be sure to print that in - # the correct color.) - self.stdout.flush() - return - - data = "".join(self._buffer) - - if _DEBUG_RENDER_OUTPUT: - self.LOG.write(("%r" % data).encode("utf-8") + b"\n") - self.LOG.flush() - - # Print characters one by one. This appears to be the best solution - # in oder to avoid traces of vertical lines when the completion - # menu disappears. - for b in data: - written = DWORD() - - retval = windll.kernel32.WriteConsoleW( - self.hconsole, b, 1, byref(written), None - ) - assert retval != 0 - - self._buffer = [] - - def get_rows_below_cursor_position(self) -> int: - info = self.get_win32_screen_buffer_info() - return info.srWindow.Bottom - info.dwCursorPosition.Y + 1 - - def scroll_buffer_to_prompt(self) -> None: - """ - To be called before drawing the prompt. This should scroll the console - to left, with the cursor at the bottom (if possible). - """ - # Get current window size - info = self.get_win32_screen_buffer_info() - sr = info.srWindow - cursor_pos = info.dwCursorPosition - - result = SMALL_RECT() - - # Scroll to the left. - result.Left = 0 - result.Right = sr.Right - sr.Left - - # Scroll vertical - win_height = sr.Bottom - sr.Top - if 0 < sr.Bottom - cursor_pos.Y < win_height - 1: - # no vertical scroll if cursor already on the screen - result.Bottom = sr.Bottom - else: - result.Bottom = max(win_height, cursor_pos.Y) - result.Top = result.Bottom - win_height - - # Scroll API - self._winapi( - windll.kernel32.SetConsoleWindowInfo, self.hconsole, True, byref(result) - ) - - def enter_alternate_screen(self) -> None: - """ - Go to alternate screen buffer. - """ - if not self._in_alternate_screen: - GENERIC_READ = 0x80000000 - GENERIC_WRITE = 0x40000000 - - # Create a new console buffer and activate that one. - handle = HANDLE( - self._winapi( - windll.kernel32.CreateConsoleScreenBuffer, - GENERIC_READ | GENERIC_WRITE, - DWORD(0), - None, - DWORD(1), - None, - ) - ) - - self._winapi(windll.kernel32.SetConsoleActiveScreenBuffer, handle) - self.hconsole = handle - self._in_alternate_screen = True - - def quit_alternate_screen(self) -> None: - """ - Make stdout again the active buffer. - """ - if self._in_alternate_screen: - stdout = HANDLE( - self._winapi(windll.kernel32.GetStdHandle, STD_OUTPUT_HANDLE) - ) - self._winapi(windll.kernel32.SetConsoleActiveScreenBuffer, stdout) - self._winapi(windll.kernel32.CloseHandle, self.hconsole) - self.hconsole = stdout - self._in_alternate_screen = False - - def enable_mouse_support(self) -> None: - ENABLE_MOUSE_INPUT = 0x10 - - # This `ENABLE_QUICK_EDIT_MODE` flag needs to be cleared for mouse - # support to work, but it's possible that it was already cleared - # before. - ENABLE_QUICK_EDIT_MODE = 0x0040 - - handle = HANDLE(windll.kernel32.GetStdHandle(STD_INPUT_HANDLE)) - - original_mode = DWORD() - self._winapi(windll.kernel32.GetConsoleMode, handle, pointer(original_mode)) - self._winapi( - windll.kernel32.SetConsoleMode, - handle, - (original_mode.value | ENABLE_MOUSE_INPUT) & ~ENABLE_QUICK_EDIT_MODE, - ) - - def disable_mouse_support(self) -> None: - ENABLE_MOUSE_INPUT = 0x10 - handle = HANDLE(windll.kernel32.GetStdHandle(STD_INPUT_HANDLE)) - - original_mode = DWORD() - self._winapi(windll.kernel32.GetConsoleMode, handle, pointer(original_mode)) - self._winapi( - windll.kernel32.SetConsoleMode, - handle, - original_mode.value & ~ENABLE_MOUSE_INPUT, - ) - - def hide_cursor(self) -> None: - pass - - def show_cursor(self) -> None: - pass - +from prompt_toolkit.data_structures import Size +from prompt_toolkit.styles import ANSI_COLOR_NAMES, Attrs +from prompt_toolkit.utils import get_cwidth +from prompt_toolkit.win32_types import ( + CONSOLE_SCREEN_BUFFER_INFO, + COORD, + SMALL_RECT, + STD_INPUT_HANDLE, + STD_OUTPUT_HANDLE, +) + +from .base import Output +from .color_depth import ColorDepth + +__all__ = [ + "Win32Output", +] + + +def _coord_byval(coord: COORD) -> c_long: + """ + Turns a COORD object into a c_long. + This will cause it to be passed by value instead of by reference. (That is what I think at least.) + + When running ``ptipython`` is run (only with IPython), we often got the following error:: + + Error in 'SetConsoleCursorPosition'. + ArgumentError("argument 2: <class 'TypeError'>: wrong type",) + argument 2: <class 'TypeError'>: wrong type + + It was solved by turning ``COORD`` parameters into a ``c_long`` like this. + + More info: http://msdn.microsoft.com/en-us/library/windows/desktop/ms686025(v=vs.85).aspx + """ + return c_long(coord.Y * 0x10000 | coord.X & 0xFFFF) + + +#: If True: write the output of the renderer also to the following file. This +#: is very useful for debugging. (e.g.: to see that we don't write more bytes +#: than required.) +_DEBUG_RENDER_OUTPUT = False +_DEBUG_RENDER_OUTPUT_FILENAME = r"prompt-toolkit-windows-output.log" + + +class NoConsoleScreenBufferError(Exception): + """ + Raised when the application is not running inside a Windows Console, but + the user tries to instantiate Win32Output. + """ + + def __init__(self) -> None: + # Are we running in 'xterm' on Windows, like git-bash for instance? + xterm = "xterm" in os.environ.get("TERM", "") + + if xterm: + message = ( + "Found %s, while expecting a Windows console. " + 'Maybe try to run this program using "winpty" ' + "or run it in cmd.exe instead. Or otherwise, " + "in case of Cygwin, use the Python executable " + "that is compiled for Cygwin." % os.environ["TERM"] + ) + else: + message = "No Windows console found. Are you running cmd.exe?" + super().__init__(message) + + +_T = TypeVar("_T") + + +class Win32Output(Output): + """ + I/O abstraction for rendering to Windows consoles. + (cmd.exe and similar.) + """ + + def __init__( + self, + stdout: TextIO, + use_complete_width: bool = False, + default_color_depth: Optional[ColorDepth] = None, + ) -> None: + self.use_complete_width = use_complete_width + self.default_color_depth = default_color_depth + + self._buffer: List[str] = [] + self.stdout: TextIO = stdout + self.hconsole = HANDLE(windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE)) + + self._in_alternate_screen = False + self._hidden = False + + self.color_lookup_table = ColorLookupTable() + + # Remember the default console colors. + info = self.get_win32_screen_buffer_info() + self.default_attrs = info.wAttributes if info else 15 + + if _DEBUG_RENDER_OUTPUT: + self.LOG = open(_DEBUG_RENDER_OUTPUT_FILENAME, "ab") + + def fileno(self) -> int: + "Return file descriptor." + return self.stdout.fileno() + + def encoding(self) -> str: + "Return encoding used for stdout." + return self.stdout.encoding + + def write(self, data: str) -> None: + if self._hidden: + data = " " * get_cwidth(data) + + self._buffer.append(data) + + def write_raw(self, data: str) -> None: + "For win32, there is no difference between write and write_raw." + self.write(data) + + def get_size(self) -> Size: + info = self.get_win32_screen_buffer_info() + + # We take the width of the *visible* region as the size. Not the width + # of the complete screen buffer. (Unless use_complete_width has been + # set.) + if self.use_complete_width: + width = info.dwSize.X + else: + width = info.srWindow.Right - info.srWindow.Left + + height = info.srWindow.Bottom - info.srWindow.Top + 1 + + # We avoid the right margin, windows will wrap otherwise. + maxwidth = info.dwSize.X - 1 + width = min(maxwidth, width) + + # Create `Size` object. + return Size(rows=height, columns=width) + + def _winapi(self, func: Callable[..., _T], *a: object, **kw: object) -> _T: + """ + Flush and call win API function. + """ + self.flush() + + if _DEBUG_RENDER_OUTPUT: + self.LOG.write(("%r" % func.__name__).encode("utf-8") + b"\n") + self.LOG.write( + b" " + ", ".join(["%r" % i for i in a]).encode("utf-8") + b"\n" + ) + self.LOG.write( + b" " + + ", ".join(["%r" % type(i) for i in a]).encode("utf-8") + + b"\n" + ) + self.LOG.flush() + + try: + return func(*a, **kw) + except ArgumentError as e: + if _DEBUG_RENDER_OUTPUT: + self.LOG.write( + (" Error in %r %r %s\n" % (func.__name__, e, e)).encode("utf-8") + ) + + raise + + def get_win32_screen_buffer_info(self) -> CONSOLE_SCREEN_BUFFER_INFO: + """ + Return Screen buffer info. + """ + # NOTE: We don't call the `GetConsoleScreenBufferInfo` API through + # `self._winapi`. Doing so causes Python to crash on certain 64bit + # Python versions. (Reproduced with 64bit Python 2.7.6, on Windows + # 10). It is not clear why. Possibly, it has to do with passing + # these objects as an argument, or through *args. + + # The Python documentation contains the following - possibly related - warning: + # ctypes does not support passing unions or structures with + # bit-fields to functions by value. While this may work on 32-bit + # x86, it's not guaranteed by the library to work in the general + # case. Unions and structures with bit-fields should always be + # passed to functions by pointer. + + # Also see: + # - https://github.com/ipython/ipython/issues/10070 + # - https://github.com/jonathanslenders/python-prompt-toolkit/issues/406 + # - https://github.com/jonathanslenders/python-prompt-toolkit/issues/86 + + self.flush() + sbinfo = CONSOLE_SCREEN_BUFFER_INFO() + success = windll.kernel32.GetConsoleScreenBufferInfo( + self.hconsole, byref(sbinfo) + ) + + # success = self._winapi(windll.kernel32.GetConsoleScreenBufferInfo, + # self.hconsole, byref(sbinfo)) + + if success: + return sbinfo + else: + raise NoConsoleScreenBufferError + + def set_title(self, title: str) -> None: + """ + Set terminal title. + """ + self._winapi(windll.kernel32.SetConsoleTitleW, title) + + def clear_title(self) -> None: + self._winapi(windll.kernel32.SetConsoleTitleW, "") + + def erase_screen(self) -> None: + start = COORD(0, 0) + sbinfo = self.get_win32_screen_buffer_info() + length = sbinfo.dwSize.X * sbinfo.dwSize.Y + + self.cursor_goto(row=0, column=0) + self._erase(start, length) + + def erase_down(self) -> None: + sbinfo = self.get_win32_screen_buffer_info() + size = sbinfo.dwSize + + start = sbinfo.dwCursorPosition + length = (size.X - size.X) + size.X * (size.Y - sbinfo.dwCursorPosition.Y) + + self._erase(start, length) + + def erase_end_of_line(self) -> None: + """""" + sbinfo = self.get_win32_screen_buffer_info() + start = sbinfo.dwCursorPosition + length = sbinfo.dwSize.X - sbinfo.dwCursorPosition.X + + self._erase(start, length) + + def _erase(self, start: COORD, length: int) -> None: + chars_written = c_ulong() + + self._winapi( + windll.kernel32.FillConsoleOutputCharacterA, + self.hconsole, + c_char(b" "), + DWORD(length), + _coord_byval(start), + byref(chars_written), + ) + + # Reset attributes. + sbinfo = self.get_win32_screen_buffer_info() + self._winapi( + windll.kernel32.FillConsoleOutputAttribute, + self.hconsole, + sbinfo.wAttributes, + length, + _coord_byval(start), + byref(chars_written), + ) + + def reset_attributes(self) -> None: + "Reset the console foreground/background color." + self._winapi( + windll.kernel32.SetConsoleTextAttribute, self.hconsole, self.default_attrs + ) + self._hidden = False + + def set_attributes(self, attrs: Attrs, color_depth: ColorDepth) -> None: + ( + fgcolor, + bgcolor, + bold, + underline, + strike, + italic, + blink, + reverse, + hidden, + ) = attrs + self._hidden = bool(hidden) + + # Start from the default attributes. + win_attrs: int = self.default_attrs + + if color_depth != ColorDepth.DEPTH_1_BIT: + # Override the last four bits: foreground color. + if fgcolor: + win_attrs = win_attrs & ~0xF + win_attrs |= self.color_lookup_table.lookup_fg_color(fgcolor) + + # Override the next four bits: background color. + if bgcolor: + win_attrs = win_attrs & ~0xF0 + win_attrs |= self.color_lookup_table.lookup_bg_color(bgcolor) + + # Reverse: swap these four bits groups. + if reverse: + win_attrs = ( + (win_attrs & ~0xFF) + | ((win_attrs & 0xF) << 4) + | ((win_attrs & 0xF0) >> 4) + ) + + self._winapi(windll.kernel32.SetConsoleTextAttribute, self.hconsole, win_attrs) + + def disable_autowrap(self) -> None: + # Not supported by Windows. + pass + + def enable_autowrap(self) -> None: + # Not supported by Windows. + pass + + def cursor_goto(self, row: int = 0, column: int = 0) -> None: + pos = COORD(X=column, Y=row) + self._winapi( + windll.kernel32.SetConsoleCursorPosition, self.hconsole, _coord_byval(pos) + ) + + def cursor_up(self, amount: int) -> None: + sr = self.get_win32_screen_buffer_info().dwCursorPosition + pos = COORD(X=sr.X, Y=sr.Y - amount) + self._winapi( + windll.kernel32.SetConsoleCursorPosition, self.hconsole, _coord_byval(pos) + ) + + def cursor_down(self, amount: int) -> None: + self.cursor_up(-amount) + + def cursor_forward(self, amount: int) -> None: + sr = self.get_win32_screen_buffer_info().dwCursorPosition + # assert sr.X + amount >= 0, 'Negative cursor position: x=%r amount=%r' % (sr.X, amount) + + pos = COORD(X=max(0, sr.X + amount), Y=sr.Y) + self._winapi( + windll.kernel32.SetConsoleCursorPosition, self.hconsole, _coord_byval(pos) + ) + + def cursor_backward(self, amount: int) -> None: + self.cursor_forward(-amount) + + def flush(self) -> None: + """ + Write to output stream and flush. + """ + if not self._buffer: + # Only flush stdout buffer. (It could be that Python still has + # something in its buffer. -- We want to be sure to print that in + # the correct color.) + self.stdout.flush() + return + + data = "".join(self._buffer) + + if _DEBUG_RENDER_OUTPUT: + self.LOG.write(("%r" % data).encode("utf-8") + b"\n") + self.LOG.flush() + + # Print characters one by one. This appears to be the best solution + # in oder to avoid traces of vertical lines when the completion + # menu disappears. + for b in data: + written = DWORD() + + retval = windll.kernel32.WriteConsoleW( + self.hconsole, b, 1, byref(written), None + ) + assert retval != 0 + + self._buffer = [] + + def get_rows_below_cursor_position(self) -> int: + info = self.get_win32_screen_buffer_info() + return info.srWindow.Bottom - info.dwCursorPosition.Y + 1 + + def scroll_buffer_to_prompt(self) -> None: + """ + To be called before drawing the prompt. This should scroll the console + to left, with the cursor at the bottom (if possible). + """ + # Get current window size + info = self.get_win32_screen_buffer_info() + sr = info.srWindow + cursor_pos = info.dwCursorPosition + + result = SMALL_RECT() + + # Scroll to the left. + result.Left = 0 + result.Right = sr.Right - sr.Left + + # Scroll vertical + win_height = sr.Bottom - sr.Top + if 0 < sr.Bottom - cursor_pos.Y < win_height - 1: + # no vertical scroll if cursor already on the screen + result.Bottom = sr.Bottom + else: + result.Bottom = max(win_height, cursor_pos.Y) + result.Top = result.Bottom - win_height + + # Scroll API + self._winapi( + windll.kernel32.SetConsoleWindowInfo, self.hconsole, True, byref(result) + ) + + def enter_alternate_screen(self) -> None: + """ + Go to alternate screen buffer. + """ + if not self._in_alternate_screen: + GENERIC_READ = 0x80000000 + GENERIC_WRITE = 0x40000000 + + # Create a new console buffer and activate that one. + handle = HANDLE( + self._winapi( + windll.kernel32.CreateConsoleScreenBuffer, + GENERIC_READ | GENERIC_WRITE, + DWORD(0), + None, + DWORD(1), + None, + ) + ) + + self._winapi(windll.kernel32.SetConsoleActiveScreenBuffer, handle) + self.hconsole = handle + self._in_alternate_screen = True + + def quit_alternate_screen(self) -> None: + """ + Make stdout again the active buffer. + """ + if self._in_alternate_screen: + stdout = HANDLE( + self._winapi(windll.kernel32.GetStdHandle, STD_OUTPUT_HANDLE) + ) + self._winapi(windll.kernel32.SetConsoleActiveScreenBuffer, stdout) + self._winapi(windll.kernel32.CloseHandle, self.hconsole) + self.hconsole = stdout + self._in_alternate_screen = False + + def enable_mouse_support(self) -> None: + ENABLE_MOUSE_INPUT = 0x10 + + # This `ENABLE_QUICK_EDIT_MODE` flag needs to be cleared for mouse + # support to work, but it's possible that it was already cleared + # before. + ENABLE_QUICK_EDIT_MODE = 0x0040 + + handle = HANDLE(windll.kernel32.GetStdHandle(STD_INPUT_HANDLE)) + + original_mode = DWORD() + self._winapi(windll.kernel32.GetConsoleMode, handle, pointer(original_mode)) + self._winapi( + windll.kernel32.SetConsoleMode, + handle, + (original_mode.value | ENABLE_MOUSE_INPUT) & ~ENABLE_QUICK_EDIT_MODE, + ) + + def disable_mouse_support(self) -> None: + ENABLE_MOUSE_INPUT = 0x10 + handle = HANDLE(windll.kernel32.GetStdHandle(STD_INPUT_HANDLE)) + + original_mode = DWORD() + self._winapi(windll.kernel32.GetConsoleMode, handle, pointer(original_mode)) + self._winapi( + windll.kernel32.SetConsoleMode, + handle, + original_mode.value & ~ENABLE_MOUSE_INPUT, + ) + + def hide_cursor(self) -> None: + pass + + def show_cursor(self) -> None: + pass + def set_cursor_shape(self, cursor_shape: CursorShape) -> None: pass def reset_cursor_shape(self) -> None: pass - @classmethod - def win32_refresh_window(cls) -> None: - """ - Call win32 API to refresh the whole Window. - - This is sometimes necessary when the application paints background - for completion menus. When the menu disappears, it leaves traces due - to a bug in the Windows Console. Sending a repaint request solves it. - """ - # Get console handle - handle = HANDLE(windll.kernel32.GetConsoleWindow()) - - RDW_INVALIDATE = 0x0001 - windll.user32.RedrawWindow(handle, None, None, c_uint(RDW_INVALIDATE)) - - def get_default_color_depth(self) -> ColorDepth: - """ - Return the default color depth for a windows terminal. - - Contrary to the Vt100 implementation, this doesn't depend on a $TERM - variable. - """ - if self.default_color_depth is not None: - return self.default_color_depth - - # For now, by default, always use 4 bit color on Windows 10 by default, - # even when vt100 escape sequences with - # ENABLE_VIRTUAL_TERMINAL_PROCESSING are supported. We don't have a - # reliable way yet to know whether our console supports true color or - # only 4-bit. - return ColorDepth.DEPTH_4_BIT - - -class FOREGROUND_COLOR: - BLACK = 0x0000 - BLUE = 0x0001 - GREEN = 0x0002 - CYAN = 0x0003 - RED = 0x0004 - MAGENTA = 0x0005 - YELLOW = 0x0006 - GRAY = 0x0007 - INTENSITY = 0x0008 # Foreground color is intensified. - - -class BACKGROUND_COLOR: - BLACK = 0x0000 - BLUE = 0x0010 - GREEN = 0x0020 - CYAN = 0x0030 - RED = 0x0040 - MAGENTA = 0x0050 - YELLOW = 0x0060 - GRAY = 0x0070 - INTENSITY = 0x0080 # Background color is intensified. - - -def _create_ansi_color_dict( - color_cls: Union[Type[FOREGROUND_COLOR], Type[BACKGROUND_COLOR]] -) -> Dict[str, int]: - "Create a table that maps the 16 named ansi colors to their Windows code." - return { - "ansidefault": color_cls.BLACK, - "ansiblack": color_cls.BLACK, - "ansigray": color_cls.GRAY, - "ansibrightblack": color_cls.BLACK | color_cls.INTENSITY, - "ansiwhite": color_cls.GRAY | color_cls.INTENSITY, - # Low intensity. - "ansired": color_cls.RED, - "ansigreen": color_cls.GREEN, - "ansiyellow": color_cls.YELLOW, - "ansiblue": color_cls.BLUE, - "ansimagenta": color_cls.MAGENTA, - "ansicyan": color_cls.CYAN, - # High intensity. - "ansibrightred": color_cls.RED | color_cls.INTENSITY, - "ansibrightgreen": color_cls.GREEN | color_cls.INTENSITY, - "ansibrightyellow": color_cls.YELLOW | color_cls.INTENSITY, - "ansibrightblue": color_cls.BLUE | color_cls.INTENSITY, - "ansibrightmagenta": color_cls.MAGENTA | color_cls.INTENSITY, - "ansibrightcyan": color_cls.CYAN | color_cls.INTENSITY, - } - - -FG_ANSI_COLORS = _create_ansi_color_dict(FOREGROUND_COLOR) -BG_ANSI_COLORS = _create_ansi_color_dict(BACKGROUND_COLOR) - -assert set(FG_ANSI_COLORS) == set(ANSI_COLOR_NAMES) -assert set(BG_ANSI_COLORS) == set(ANSI_COLOR_NAMES) - - -class ColorLookupTable: - """ - Inspired by pygments/formatters/terminal256.py - """ - - def __init__(self) -> None: - self._win32_colors = self._build_color_table() - - # Cache (map color string to foreground and background code). - self.best_match: Dict[str, Tuple[int, int]] = {} - - @staticmethod - def _build_color_table() -> List[Tuple[int, int, int, int, int]]: - """ - Build an RGB-to-256 color conversion table - """ - FG = FOREGROUND_COLOR - BG = BACKGROUND_COLOR - - return [ - (0x00, 0x00, 0x00, FG.BLACK, BG.BLACK), - (0x00, 0x00, 0xAA, FG.BLUE, BG.BLUE), - (0x00, 0xAA, 0x00, FG.GREEN, BG.GREEN), - (0x00, 0xAA, 0xAA, FG.CYAN, BG.CYAN), - (0xAA, 0x00, 0x00, FG.RED, BG.RED), - (0xAA, 0x00, 0xAA, FG.MAGENTA, BG.MAGENTA), - (0xAA, 0xAA, 0x00, FG.YELLOW, BG.YELLOW), - (0x88, 0x88, 0x88, FG.GRAY, BG.GRAY), - (0x44, 0x44, 0xFF, FG.BLUE | FG.INTENSITY, BG.BLUE | BG.INTENSITY), - (0x44, 0xFF, 0x44, FG.GREEN | FG.INTENSITY, BG.GREEN | BG.INTENSITY), - (0x44, 0xFF, 0xFF, FG.CYAN | FG.INTENSITY, BG.CYAN | BG.INTENSITY), - (0xFF, 0x44, 0x44, FG.RED | FG.INTENSITY, BG.RED | BG.INTENSITY), - (0xFF, 0x44, 0xFF, FG.MAGENTA | FG.INTENSITY, BG.MAGENTA | BG.INTENSITY), - (0xFF, 0xFF, 0x44, FG.YELLOW | FG.INTENSITY, BG.YELLOW | BG.INTENSITY), - (0x44, 0x44, 0x44, FG.BLACK | FG.INTENSITY, BG.BLACK | BG.INTENSITY), - (0xFF, 0xFF, 0xFF, FG.GRAY | FG.INTENSITY, BG.GRAY | BG.INTENSITY), - ] - - def _closest_color(self, r: int, g: int, b: int) -> Tuple[int, int]: - distance = 257 * 257 * 3 # "infinity" (>distance from #000000 to #ffffff) - fg_match = 0 - bg_match = 0 - - for r_, g_, b_, fg_, bg_ in self._win32_colors: - rd = r - r_ - gd = g - g_ - bd = b - b_ - - d = rd * rd + gd * gd + bd * bd - - if d < distance: - fg_match = fg_ - bg_match = bg_ - distance = d - return fg_match, bg_match - - def _color_indexes(self, color: str) -> Tuple[int, int]: - indexes = self.best_match.get(color, None) - if indexes is None: - try: - rgb = int(str(color), 16) - except ValueError: - rgb = 0 - - r = (rgb >> 16) & 0xFF - g = (rgb >> 8) & 0xFF - b = rgb & 0xFF - indexes = self._closest_color(r, g, b) - self.best_match[color] = indexes - return indexes - - def lookup_fg_color(self, fg_color: str) -> int: - """ - Return the color for use in the - `windll.kernel32.SetConsoleTextAttribute` API call. - - :param fg_color: Foreground as text. E.g. 'ffffff' or 'red' - """ - # Foreground. - if fg_color in FG_ANSI_COLORS: - return FG_ANSI_COLORS[fg_color] - else: - return self._color_indexes(fg_color)[0] - - def lookup_bg_color(self, bg_color: str) -> int: - """ - Return the color for use in the - `windll.kernel32.SetConsoleTextAttribute` API call. - - :param bg_color: Background as text. E.g. 'ffffff' or 'red' - """ - # Background. - if bg_color in BG_ANSI_COLORS: - return BG_ANSI_COLORS[bg_color] - else: - return self._color_indexes(bg_color)[1] + @classmethod + def win32_refresh_window(cls) -> None: + """ + Call win32 API to refresh the whole Window. + + This is sometimes necessary when the application paints background + for completion menus. When the menu disappears, it leaves traces due + to a bug in the Windows Console. Sending a repaint request solves it. + """ + # Get console handle + handle = HANDLE(windll.kernel32.GetConsoleWindow()) + + RDW_INVALIDATE = 0x0001 + windll.user32.RedrawWindow(handle, None, None, c_uint(RDW_INVALIDATE)) + + def get_default_color_depth(self) -> ColorDepth: + """ + Return the default color depth for a windows terminal. + + Contrary to the Vt100 implementation, this doesn't depend on a $TERM + variable. + """ + if self.default_color_depth is not None: + return self.default_color_depth + + # For now, by default, always use 4 bit color on Windows 10 by default, + # even when vt100 escape sequences with + # ENABLE_VIRTUAL_TERMINAL_PROCESSING are supported. We don't have a + # reliable way yet to know whether our console supports true color or + # only 4-bit. + return ColorDepth.DEPTH_4_BIT + + +class FOREGROUND_COLOR: + BLACK = 0x0000 + BLUE = 0x0001 + GREEN = 0x0002 + CYAN = 0x0003 + RED = 0x0004 + MAGENTA = 0x0005 + YELLOW = 0x0006 + GRAY = 0x0007 + INTENSITY = 0x0008 # Foreground color is intensified. + + +class BACKGROUND_COLOR: + BLACK = 0x0000 + BLUE = 0x0010 + GREEN = 0x0020 + CYAN = 0x0030 + RED = 0x0040 + MAGENTA = 0x0050 + YELLOW = 0x0060 + GRAY = 0x0070 + INTENSITY = 0x0080 # Background color is intensified. + + +def _create_ansi_color_dict( + color_cls: Union[Type[FOREGROUND_COLOR], Type[BACKGROUND_COLOR]] +) -> Dict[str, int]: + "Create a table that maps the 16 named ansi colors to their Windows code." + return { + "ansidefault": color_cls.BLACK, + "ansiblack": color_cls.BLACK, + "ansigray": color_cls.GRAY, + "ansibrightblack": color_cls.BLACK | color_cls.INTENSITY, + "ansiwhite": color_cls.GRAY | color_cls.INTENSITY, + # Low intensity. + "ansired": color_cls.RED, + "ansigreen": color_cls.GREEN, + "ansiyellow": color_cls.YELLOW, + "ansiblue": color_cls.BLUE, + "ansimagenta": color_cls.MAGENTA, + "ansicyan": color_cls.CYAN, + # High intensity. + "ansibrightred": color_cls.RED | color_cls.INTENSITY, + "ansibrightgreen": color_cls.GREEN | color_cls.INTENSITY, + "ansibrightyellow": color_cls.YELLOW | color_cls.INTENSITY, + "ansibrightblue": color_cls.BLUE | color_cls.INTENSITY, + "ansibrightmagenta": color_cls.MAGENTA | color_cls.INTENSITY, + "ansibrightcyan": color_cls.CYAN | color_cls.INTENSITY, + } + + +FG_ANSI_COLORS = _create_ansi_color_dict(FOREGROUND_COLOR) +BG_ANSI_COLORS = _create_ansi_color_dict(BACKGROUND_COLOR) + +assert set(FG_ANSI_COLORS) == set(ANSI_COLOR_NAMES) +assert set(BG_ANSI_COLORS) == set(ANSI_COLOR_NAMES) + + +class ColorLookupTable: + """ + Inspired by pygments/formatters/terminal256.py + """ + + def __init__(self) -> None: + self._win32_colors = self._build_color_table() + + # Cache (map color string to foreground and background code). + self.best_match: Dict[str, Tuple[int, int]] = {} + + @staticmethod + def _build_color_table() -> List[Tuple[int, int, int, int, int]]: + """ + Build an RGB-to-256 color conversion table + """ + FG = FOREGROUND_COLOR + BG = BACKGROUND_COLOR + + return [ + (0x00, 0x00, 0x00, FG.BLACK, BG.BLACK), + (0x00, 0x00, 0xAA, FG.BLUE, BG.BLUE), + (0x00, 0xAA, 0x00, FG.GREEN, BG.GREEN), + (0x00, 0xAA, 0xAA, FG.CYAN, BG.CYAN), + (0xAA, 0x00, 0x00, FG.RED, BG.RED), + (0xAA, 0x00, 0xAA, FG.MAGENTA, BG.MAGENTA), + (0xAA, 0xAA, 0x00, FG.YELLOW, BG.YELLOW), + (0x88, 0x88, 0x88, FG.GRAY, BG.GRAY), + (0x44, 0x44, 0xFF, FG.BLUE | FG.INTENSITY, BG.BLUE | BG.INTENSITY), + (0x44, 0xFF, 0x44, FG.GREEN | FG.INTENSITY, BG.GREEN | BG.INTENSITY), + (0x44, 0xFF, 0xFF, FG.CYAN | FG.INTENSITY, BG.CYAN | BG.INTENSITY), + (0xFF, 0x44, 0x44, FG.RED | FG.INTENSITY, BG.RED | BG.INTENSITY), + (0xFF, 0x44, 0xFF, FG.MAGENTA | FG.INTENSITY, BG.MAGENTA | BG.INTENSITY), + (0xFF, 0xFF, 0x44, FG.YELLOW | FG.INTENSITY, BG.YELLOW | BG.INTENSITY), + (0x44, 0x44, 0x44, FG.BLACK | FG.INTENSITY, BG.BLACK | BG.INTENSITY), + (0xFF, 0xFF, 0xFF, FG.GRAY | FG.INTENSITY, BG.GRAY | BG.INTENSITY), + ] + + def _closest_color(self, r: int, g: int, b: int) -> Tuple[int, int]: + distance = 257 * 257 * 3 # "infinity" (>distance from #000000 to #ffffff) + fg_match = 0 + bg_match = 0 + + for r_, g_, b_, fg_, bg_ in self._win32_colors: + rd = r - r_ + gd = g - g_ + bd = b - b_ + + d = rd * rd + gd * gd + bd * bd + + if d < distance: + fg_match = fg_ + bg_match = bg_ + distance = d + return fg_match, bg_match + + def _color_indexes(self, color: str) -> Tuple[int, int]: + indexes = self.best_match.get(color, None) + if indexes is None: + try: + rgb = int(str(color), 16) + except ValueError: + rgb = 0 + + r = (rgb >> 16) & 0xFF + g = (rgb >> 8) & 0xFF + b = rgb & 0xFF + indexes = self._closest_color(r, g, b) + self.best_match[color] = indexes + return indexes + + def lookup_fg_color(self, fg_color: str) -> int: + """ + Return the color for use in the + `windll.kernel32.SetConsoleTextAttribute` API call. + + :param fg_color: Foreground as text. E.g. 'ffffff' or 'red' + """ + # Foreground. + if fg_color in FG_ANSI_COLORS: + return FG_ANSI_COLORS[fg_color] + else: + return self._color_indexes(fg_color)[0] + + def lookup_bg_color(self, bg_color: str) -> int: + """ + Return the color for use in the + `windll.kernel32.SetConsoleTextAttribute` API call. + + :param bg_color: Background as text. E.g. 'ffffff' or 'red' + """ + # Background. + if bg_color in BG_ANSI_COLORS: + return BG_ANSI_COLORS[bg_color] + else: + return self._color_indexes(bg_color)[1] diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/windows10.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/windows10.py index dc4f7cdf44..933f54a28f 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/windows10.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/output/windows10.py @@ -1,104 +1,104 @@ -from ctypes import byref, windll -from ctypes.wintypes import DWORD, HANDLE -from typing import Any, Optional, TextIO - -from prompt_toolkit.data_structures import Size -from prompt_toolkit.utils import is_windows -from prompt_toolkit.win32_types import STD_OUTPUT_HANDLE - -from .base import Output -from .color_depth import ColorDepth -from .vt100 import Vt100_Output -from .win32 import Win32Output - -__all__ = [ - "Windows10_Output", -] - -# See: https://msdn.microsoft.com/pl-pl/library/windows/desktop/ms686033(v=vs.85).aspx -ENABLE_PROCESSED_INPUT = 0x0001 -ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 - - -class Windows10_Output: - """ - Windows 10 output abstraction. This enables and uses vt100 escape sequences. - """ - - def __init__( - self, stdout: TextIO, default_color_depth: Optional[ColorDepth] = None - ) -> None: - self.win32_output = Win32Output(stdout, default_color_depth=default_color_depth) - self.vt100_output = Vt100_Output( - stdout, lambda: Size(0, 0), default_color_depth=default_color_depth - ) - self._hconsole = HANDLE(windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE)) - - def flush(self) -> None: - """ - Write to output stream and flush. - """ - original_mode = DWORD(0) - - # Remember the previous console mode. - windll.kernel32.GetConsoleMode(self._hconsole, byref(original_mode)) - - # Enable processing of vt100 sequences. - windll.kernel32.SetConsoleMode( - self._hconsole, - DWORD(ENABLE_PROCESSED_INPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING), - ) - - try: - self.vt100_output.flush() - finally: - # Restore console mode. - windll.kernel32.SetConsoleMode(self._hconsole, original_mode) - - @property - def responds_to_cpr(self) -> bool: - return False # We don't need this on Windows. - - def __getattr__(self, name: str) -> Any: - if name in ( - "get_size", - "get_rows_below_cursor_position", - "enable_mouse_support", - "disable_mouse_support", - "scroll_buffer_to_prompt", - "get_win32_screen_buffer_info", - "enable_bracketed_paste", - "disable_bracketed_paste", - "get_default_color_depth", - ): - return getattr(self.win32_output, name) - else: - return getattr(self.vt100_output, name) - - -Output.register(Windows10_Output) - - -def is_win_vt100_enabled() -> bool: - """ - Returns True when we're running Windows and VT100 escape sequences are - supported. - """ - if not is_windows(): - return False - - hconsole = HANDLE(windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE)) - - # Get original console mode. - original_mode = DWORD(0) - windll.kernel32.GetConsoleMode(hconsole, byref(original_mode)) - - try: - # Try to enable VT100 sequences. - result: int = windll.kernel32.SetConsoleMode( - hconsole, DWORD(ENABLE_PROCESSED_INPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING) - ) - - return result == 1 - finally: - windll.kernel32.SetConsoleMode(hconsole, original_mode) +from ctypes import byref, windll +from ctypes.wintypes import DWORD, HANDLE +from typing import Any, Optional, TextIO + +from prompt_toolkit.data_structures import Size +from prompt_toolkit.utils import is_windows +from prompt_toolkit.win32_types import STD_OUTPUT_HANDLE + +from .base import Output +from .color_depth import ColorDepth +from .vt100 import Vt100_Output +from .win32 import Win32Output + +__all__ = [ + "Windows10_Output", +] + +# See: https://msdn.microsoft.com/pl-pl/library/windows/desktop/ms686033(v=vs.85).aspx +ENABLE_PROCESSED_INPUT = 0x0001 +ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 + + +class Windows10_Output: + """ + Windows 10 output abstraction. This enables and uses vt100 escape sequences. + """ + + def __init__( + self, stdout: TextIO, default_color_depth: Optional[ColorDepth] = None + ) -> None: + self.win32_output = Win32Output(stdout, default_color_depth=default_color_depth) + self.vt100_output = Vt100_Output( + stdout, lambda: Size(0, 0), default_color_depth=default_color_depth + ) + self._hconsole = HANDLE(windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE)) + + def flush(self) -> None: + """ + Write to output stream and flush. + """ + original_mode = DWORD(0) + + # Remember the previous console mode. + windll.kernel32.GetConsoleMode(self._hconsole, byref(original_mode)) + + # Enable processing of vt100 sequences. + windll.kernel32.SetConsoleMode( + self._hconsole, + DWORD(ENABLE_PROCESSED_INPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING), + ) + + try: + self.vt100_output.flush() + finally: + # Restore console mode. + windll.kernel32.SetConsoleMode(self._hconsole, original_mode) + + @property + def responds_to_cpr(self) -> bool: + return False # We don't need this on Windows. + + def __getattr__(self, name: str) -> Any: + if name in ( + "get_size", + "get_rows_below_cursor_position", + "enable_mouse_support", + "disable_mouse_support", + "scroll_buffer_to_prompt", + "get_win32_screen_buffer_info", + "enable_bracketed_paste", + "disable_bracketed_paste", + "get_default_color_depth", + ): + return getattr(self.win32_output, name) + else: + return getattr(self.vt100_output, name) + + +Output.register(Windows10_Output) + + +def is_win_vt100_enabled() -> bool: + """ + Returns True when we're running Windows and VT100 escape sequences are + supported. + """ + if not is_windows(): + return False + + hconsole = HANDLE(windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE)) + + # Get original console mode. + original_mode = DWORD(0) + windll.kernel32.GetConsoleMode(hconsole, byref(original_mode)) + + try: + # Try to enable VT100 sequences. + result: int = windll.kernel32.SetConsoleMode( + hconsole, DWORD(ENABLE_PROCESSED_INPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING) + ) + + return result == 1 + finally: + windll.kernel32.SetConsoleMode(hconsole, original_mode) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/patch_stdout.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/patch_stdout.py index abfeb2a1a3..0abbcdb847 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/patch_stdout.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/patch_stdout.py @@ -1,288 +1,288 @@ -""" -patch_stdout -============ - -This implements a context manager that ensures that print statements within -it won't destroy the user interface. The context manager will replace -`sys.stdout` by something that draws the output above the current prompt, -rather than overwriting the UI. - -Usage:: - - with patch_stdout(application): - ... - application.run() - ... - -Multiple applications can run in the body of the context manager, one after the -other. -""" -import asyncio -import queue -import sys -import threading -import time -from contextlib import contextmanager -from typing import Generator, List, Optional, TextIO, Union, cast - -from .application import get_app_session, run_in_terminal -from .output import Output - -__all__ = [ - "patch_stdout", - "StdoutProxy", -] - - -@contextmanager -def patch_stdout(raw: bool = False) -> Generator[None, None, None]: - """ - Replace `sys.stdout` by an :class:`_StdoutProxy` instance. - - Writing to this proxy will make sure that the text appears above the - prompt, and that it doesn't destroy the output from the renderer. If no - application is curring, the behaviour should be identical to writing to - `sys.stdout` directly. - - Warning: If a new event loop is installed using `asyncio.set_event_loop()`, - then make sure that the context manager is applied after the event loop - is changed. Printing to stdout will be scheduled in the event loop - that's active when the context manager is created. - - :param raw: (`bool`) When True, vt100 terminal escape sequences are not - removed/escaped. - """ - with StdoutProxy(raw=raw) as proxy: - original_stdout = sys.stdout - original_stderr = sys.stderr - - # Enter. - sys.stdout = cast(TextIO, proxy) - sys.stderr = cast(TextIO, proxy) - - try: - yield - finally: - sys.stdout = original_stdout - sys.stderr = original_stderr - - -class _Done: - "Sentinel value for stopping the stdout proxy." - - -class StdoutProxy: - """ - File-like object, which prints everything written to it, output above the - current application/prompt. This class is compatible with other file - objects and can be used as a drop-in replacement for `sys.stdout` or can - for instance be passed to `logging.StreamHandler`. - - The current application, above which we print, is determined by looking - what application currently runs in the `AppSession` that is active during - the creation of this instance. - - This class can be used as a context manager. - - In order to avoid having to repaint the prompt continuously for every - little write, a short delay of `sleep_between_writes` seconds will be added - between writes in order to bundle many smaller writes in a short timespan. - """ - - def __init__( - self, - sleep_between_writes: float = 0.2, - raw: bool = False, - ) -> None: - - self.sleep_between_writes = sleep_between_writes - self.raw = raw - - self._lock = threading.RLock() - self._buffer: List[str] = [] - - # Keep track of the curret app session. - self.app_session = get_app_session() - - # See what output is active *right now*. We should do it at this point, - # before this `StdoutProxy` instance is possibly assigned to `sys.stdout`. - # Otherwise, if `patch_stdout` is used, and no `Output` instance has - # been created, then the default output creation code will see this - # proxy object as `sys.stdout`, and get in a recursive loop trying to - # access `StdoutProxy.isatty()` which will again retrieve the output. - self._output: Output = self.app_session.output - - # Flush thread - self._flush_queue: queue.Queue[Union[str, _Done]] = queue.Queue() - self._flush_thread = self._start_write_thread() - self.closed = False - - def __enter__(self) -> "StdoutProxy": - return self - - def __exit__(self, *args: object) -> None: - self.close() - - def close(self) -> None: - """ - Stop `StdoutProxy` proxy. - - This will terminate the write thread, make sure everything is flushed - and wait for the write thread to finish. - """ - if not self.closed: - self._flush_queue.put(_Done()) - self._flush_thread.join() - self.closed = True - - def _start_write_thread(self) -> threading.Thread: - thread = threading.Thread( - target=self._write_thread, - name="patch-stdout-flush-thread", - daemon=True, - ) - thread.start() - return thread - - def _write_thread(self) -> None: - done = False - - while not done: - item = self._flush_queue.get() - - if isinstance(item, _Done): - break - - # Don't bother calling when we got an empty string. - if not item: - continue - - text = [] - text.append(item) - - # Read the rest of the queue if more data was queued up. - while True: - try: - item = self._flush_queue.get_nowait() - except queue.Empty: - break - else: - if isinstance(item, _Done): - done = True - else: - text.append(item) - - app_loop = self._get_app_loop() - self._write_and_flush(app_loop, "".join(text)) - - # If an application was running that requires repainting, then wait - # for a very short time, in order to bundle actual writes and avoid - # having to repaint to often. - if app_loop is not None: - time.sleep(self.sleep_between_writes) - - def _get_app_loop(self) -> Optional[asyncio.AbstractEventLoop]: - """ - Return the event loop for the application currently running in our - `AppSession`. - """ - app = self.app_session.app - - if app is None: - return None - - return app.loop - - def _write_and_flush( - self, loop: Optional[asyncio.AbstractEventLoop], text: str - ) -> None: - """ - Write the given text to stdout and flush. - If an application is running, use `run_in_terminal`. - """ - - def write_and_flush() -> None: - if self.raw: - self._output.write_raw(text) - else: - self._output.write(text) - - self._output.flush() - - def write_and_flush_in_loop() -> None: - # If an application is running, use `run_in_terminal`, otherwise - # call it directly. - run_in_terminal(write_and_flush, in_executor=False) - - if loop is None: - # No loop, write immediately. - write_and_flush() - else: - # Make sure `write_and_flush` is executed *in* the event loop, not - # in another thread. - loop.call_soon_threadsafe(write_and_flush_in_loop) - - def _write(self, data: str) -> None: - """ - Note: print()-statements cause to multiple write calls. - (write('line') and write('\n')). Of course we don't want to call - `run_in_terminal` for every individual call, because that's too - expensive, and as long as the newline hasn't been written, the - text itself is again overwritten by the rendering of the input - command line. Therefor, we have a little buffer which holds the - text until a newline is written to stdout. - """ - if "\n" in data: - # When there is a newline in the data, write everything before the - # newline, including the newline itself. - before, after = data.rsplit("\n", 1) - to_write = self._buffer + [before, "\n"] - self._buffer = [after] - - text = "".join(to_write) - self._flush_queue.put(text) - else: - # Otherwise, cache in buffer. - self._buffer.append(data) - - def _flush(self) -> None: - text = "".join(self._buffer) - self._buffer = [] - self._flush_queue.put(text) - - def write(self, data: str) -> int: - with self._lock: - self._write(data) - - return len(data) # Pretend everything was written. - - def flush(self) -> None: - """ - Flush buffered output. - """ - with self._lock: - self._flush() - - @property - def original_stdout(self) -> TextIO: - return self._output.stdout or sys.__stdout__ - - # Attributes for compatibility with sys.__stdout__: - - def fileno(self) -> int: - return self._output.fileno() - - def isatty(self) -> bool: - stdout = self._output.stdout - if stdout is None: - return False - - return stdout.isatty() - - @property - def encoding(self) -> str: - return self._output.encoding() - - @property - def errors(self) -> str: - return "strict" +""" +patch_stdout +============ + +This implements a context manager that ensures that print statements within +it won't destroy the user interface. The context manager will replace +`sys.stdout` by something that draws the output above the current prompt, +rather than overwriting the UI. + +Usage:: + + with patch_stdout(application): + ... + application.run() + ... + +Multiple applications can run in the body of the context manager, one after the +other. +""" +import asyncio +import queue +import sys +import threading +import time +from contextlib import contextmanager +from typing import Generator, List, Optional, TextIO, Union, cast + +from .application import get_app_session, run_in_terminal +from .output import Output + +__all__ = [ + "patch_stdout", + "StdoutProxy", +] + + +@contextmanager +def patch_stdout(raw: bool = False) -> Generator[None, None, None]: + """ + Replace `sys.stdout` by an :class:`_StdoutProxy` instance. + + Writing to this proxy will make sure that the text appears above the + prompt, and that it doesn't destroy the output from the renderer. If no + application is curring, the behaviour should be identical to writing to + `sys.stdout` directly. + + Warning: If a new event loop is installed using `asyncio.set_event_loop()`, + then make sure that the context manager is applied after the event loop + is changed. Printing to stdout will be scheduled in the event loop + that's active when the context manager is created. + + :param raw: (`bool`) When True, vt100 terminal escape sequences are not + removed/escaped. + """ + with StdoutProxy(raw=raw) as proxy: + original_stdout = sys.stdout + original_stderr = sys.stderr + + # Enter. + sys.stdout = cast(TextIO, proxy) + sys.stderr = cast(TextIO, proxy) + + try: + yield + finally: + sys.stdout = original_stdout + sys.stderr = original_stderr + + +class _Done: + "Sentinel value for stopping the stdout proxy." + + +class StdoutProxy: + """ + File-like object, which prints everything written to it, output above the + current application/prompt. This class is compatible with other file + objects and can be used as a drop-in replacement for `sys.stdout` or can + for instance be passed to `logging.StreamHandler`. + + The current application, above which we print, is determined by looking + what application currently runs in the `AppSession` that is active during + the creation of this instance. + + This class can be used as a context manager. + + In order to avoid having to repaint the prompt continuously for every + little write, a short delay of `sleep_between_writes` seconds will be added + between writes in order to bundle many smaller writes in a short timespan. + """ + + def __init__( + self, + sleep_between_writes: float = 0.2, + raw: bool = False, + ) -> None: + + self.sleep_between_writes = sleep_between_writes + self.raw = raw + + self._lock = threading.RLock() + self._buffer: List[str] = [] + + # Keep track of the curret app session. + self.app_session = get_app_session() + + # See what output is active *right now*. We should do it at this point, + # before this `StdoutProxy` instance is possibly assigned to `sys.stdout`. + # Otherwise, if `patch_stdout` is used, and no `Output` instance has + # been created, then the default output creation code will see this + # proxy object as `sys.stdout`, and get in a recursive loop trying to + # access `StdoutProxy.isatty()` which will again retrieve the output. + self._output: Output = self.app_session.output + + # Flush thread + self._flush_queue: queue.Queue[Union[str, _Done]] = queue.Queue() + self._flush_thread = self._start_write_thread() + self.closed = False + + def __enter__(self) -> "StdoutProxy": + return self + + def __exit__(self, *args: object) -> None: + self.close() + + def close(self) -> None: + """ + Stop `StdoutProxy` proxy. + + This will terminate the write thread, make sure everything is flushed + and wait for the write thread to finish. + """ + if not self.closed: + self._flush_queue.put(_Done()) + self._flush_thread.join() + self.closed = True + + def _start_write_thread(self) -> threading.Thread: + thread = threading.Thread( + target=self._write_thread, + name="patch-stdout-flush-thread", + daemon=True, + ) + thread.start() + return thread + + def _write_thread(self) -> None: + done = False + + while not done: + item = self._flush_queue.get() + + if isinstance(item, _Done): + break + + # Don't bother calling when we got an empty string. + if not item: + continue + + text = [] + text.append(item) + + # Read the rest of the queue if more data was queued up. + while True: + try: + item = self._flush_queue.get_nowait() + except queue.Empty: + break + else: + if isinstance(item, _Done): + done = True + else: + text.append(item) + + app_loop = self._get_app_loop() + self._write_and_flush(app_loop, "".join(text)) + + # If an application was running that requires repainting, then wait + # for a very short time, in order to bundle actual writes and avoid + # having to repaint to often. + if app_loop is not None: + time.sleep(self.sleep_between_writes) + + def _get_app_loop(self) -> Optional[asyncio.AbstractEventLoop]: + """ + Return the event loop for the application currently running in our + `AppSession`. + """ + app = self.app_session.app + + if app is None: + return None + + return app.loop + + def _write_and_flush( + self, loop: Optional[asyncio.AbstractEventLoop], text: str + ) -> None: + """ + Write the given text to stdout and flush. + If an application is running, use `run_in_terminal`. + """ + + def write_and_flush() -> None: + if self.raw: + self._output.write_raw(text) + else: + self._output.write(text) + + self._output.flush() + + def write_and_flush_in_loop() -> None: + # If an application is running, use `run_in_terminal`, otherwise + # call it directly. + run_in_terminal(write_and_flush, in_executor=False) + + if loop is None: + # No loop, write immediately. + write_and_flush() + else: + # Make sure `write_and_flush` is executed *in* the event loop, not + # in another thread. + loop.call_soon_threadsafe(write_and_flush_in_loop) + + def _write(self, data: str) -> None: + """ + Note: print()-statements cause to multiple write calls. + (write('line') and write('\n')). Of course we don't want to call + `run_in_terminal` for every individual call, because that's too + expensive, and as long as the newline hasn't been written, the + text itself is again overwritten by the rendering of the input + command line. Therefor, we have a little buffer which holds the + text until a newline is written to stdout. + """ + if "\n" in data: + # When there is a newline in the data, write everything before the + # newline, including the newline itself. + before, after = data.rsplit("\n", 1) + to_write = self._buffer + [before, "\n"] + self._buffer = [after] + + text = "".join(to_write) + self._flush_queue.put(text) + else: + # Otherwise, cache in buffer. + self._buffer.append(data) + + def _flush(self) -> None: + text = "".join(self._buffer) + self._buffer = [] + self._flush_queue.put(text) + + def write(self, data: str) -> int: + with self._lock: + self._write(data) + + return len(data) # Pretend everything was written. + + def flush(self) -> None: + """ + Flush buffered output. + """ + with self._lock: + self._flush() + + @property + def original_stdout(self) -> TextIO: + return self._output.stdout or sys.__stdout__ + + # Attributes for compatibility with sys.__stdout__: + + def fileno(self) -> int: + return self._output.fileno() + + def isatty(self) -> bool: + stdout = self._output.stdout + if stdout is None: + return False + + return stdout.isatty() + + @property + def encoding(self) -> str: + return self._output.encoding() + + @property + def errors(self) -> str: + return "strict" diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/renderer.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/renderer.py index 1dc8dcc81b..d670c3c57b 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/renderer.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/renderer.py @@ -1,711 +1,711 @@ -""" -Renders the command line on the console. -(Redraws parts of the input line that were changed.) -""" -from asyncio import FIRST_COMPLETED, Future, ensure_future, sleep, wait -from collections import deque -from enum import Enum -from typing import TYPE_CHECKING, Any, Callable, Deque, Dict, Hashable, Optional, Tuple - -from prompt_toolkit.application.current import get_app +""" +Renders the command line on the console. +(Redraws parts of the input line that were changed.) +""" +from asyncio import FIRST_COMPLETED, Future, ensure_future, sleep, wait +from collections import deque +from enum import Enum +from typing import TYPE_CHECKING, Any, Callable, Deque, Dict, Hashable, Optional, Tuple + +from prompt_toolkit.application.current import get_app from prompt_toolkit.cursor_shapes import CursorShape -from prompt_toolkit.data_structures import Point, Size -from prompt_toolkit.filters import FilterOrBool, to_filter -from prompt_toolkit.formatted_text import AnyFormattedText, to_formatted_text -from prompt_toolkit.layout.mouse_handlers import MouseHandlers -from prompt_toolkit.layout.screen import Char, Screen, WritePosition -from prompt_toolkit.output import ColorDepth, Output -from prompt_toolkit.styles import ( - Attrs, - BaseStyle, - DummyStyleTransformation, - StyleTransformation, -) - -if TYPE_CHECKING: - from prompt_toolkit.application import Application - from prompt_toolkit.layout.layout import Layout - - -__all__ = [ - "Renderer", - "print_formatted_text", -] - - -def _output_screen_diff( - app: "Application[Any]", - output: Output, - screen: Screen, - current_pos: Point, - color_depth: ColorDepth, - previous_screen: Optional[Screen], - last_style: Optional[str], - is_done: bool, # XXX: drop is_done - full_screen: bool, - attrs_for_style_string: "_StyleStringToAttrsCache", - style_string_has_style: "_StyleStringHasStyleCache", - size: Size, - previous_width: int, -) -> Tuple[Point, Optional[str]]: - """ - Render the diff between this screen and the previous screen. - - This takes two `Screen` instances. The one that represents the output like - it was during the last rendering and one that represents the current - output raster. Looking at these two `Screen` instances, this function will - render the difference by calling the appropriate methods of the `Output` - object that only paint the changes to the terminal. - - This is some performance-critical code which is heavily optimized. - Don't change things without profiling first. - - :param current_pos: Current cursor position. - :param last_style: The style string, used for drawing the last drawn - character. (Color/attributes.) - :param attrs_for_style_string: :class:`._StyleStringToAttrsCache` instance. - :param width: The width of the terminal. - :param previous_width: The width of the terminal during the last rendering. - """ - width, height = size.columns, size.rows - - #: Variable for capturing the output. - write = output.write - write_raw = output.write_raw - - # Create locals for the most used output methods. - # (Save expensive attribute lookups.) - _output_set_attributes = output.set_attributes - _output_reset_attributes = output.reset_attributes - _output_cursor_forward = output.cursor_forward - _output_cursor_up = output.cursor_up - _output_cursor_backward = output.cursor_backward - - # Hide cursor before rendering. (Avoid flickering.) - output.hide_cursor() - - def reset_attributes() -> None: - "Wrapper around Output.reset_attributes." - nonlocal last_style - _output_reset_attributes() - last_style = None # Forget last char after resetting attributes. - - def move_cursor(new: Point) -> Point: - "Move cursor to this `new` point. Returns the given Point." - current_x, current_y = current_pos.x, current_pos.y - - if new.y > current_y: - # Use newlines instead of CURSOR_DOWN, because this might add new lines. - # CURSOR_DOWN will never create new lines at the bottom. - # Also reset attributes, otherwise the newline could draw a - # background color. - reset_attributes() - write("\r\n" * (new.y - current_y)) - current_x = 0 - _output_cursor_forward(new.x) - return new - elif new.y < current_y: - _output_cursor_up(current_y - new.y) - - if current_x >= width - 1: - write("\r") - _output_cursor_forward(new.x) - elif new.x < current_x or current_x >= width - 1: - _output_cursor_backward(current_x - new.x) - elif new.x > current_x: - _output_cursor_forward(new.x - current_x) - - return new - - def output_char(char: Char) -> None: - """ - Write the output of this character. - """ - nonlocal last_style - - # If the last printed character has the same style, don't output the - # style again. - if last_style == char.style: - write(char.char) - else: - # Look up `Attr` for this style string. Only set attributes if different. - # (Two style strings can still have the same formatting.) - # Note that an empty style string can have formatting that needs to - # be applied, because of style transformations. - new_attrs = attrs_for_style_string[char.style] - if not last_style or new_attrs != attrs_for_style_string[last_style]: - _output_set_attributes(new_attrs, color_depth) - - write(char.char) - last_style = char.style - - def get_max_column_index(row: Dict[int, Char]) -> int: - """ - Return max used column index, ignoring whitespace (without style) at - the end of the line. This is important for people that copy/paste - terminal output. - - There are two reasons we are sometimes seeing whitespace at the end: - - `BufferControl` adds a trailing space to each line, because it's a - possible cursor position, so that the line wrapping won't change if - the cursor position moves around. - - The `Window` adds a style class to the current line for highlighting - (cursor-line). - """ - numbers = [ - index - for index, cell in row.items() - if cell.char != " " or style_string_has_style[cell.style] - ] - numbers.append(0) - return max(numbers) - - # Render for the first time: reset styling. - if not previous_screen: - reset_attributes() - - # Disable autowrap. (When entering a the alternate screen, or anytime when - # we have a prompt. - In the case of a REPL, like IPython, people can have - # background threads, and it's hard for debugging if their output is not - # wrapped.) - if not previous_screen or not full_screen: - output.disable_autowrap() - - # When the previous screen has a different size, redraw everything anyway. - # Also when we are done. (We might take up less rows, so clearing is important.) - if ( - is_done or not previous_screen or previous_width != width - ): # XXX: also consider height?? - current_pos = move_cursor(Point(x=0, y=0)) - reset_attributes() - output.erase_down() - - previous_screen = Screen() - - # Get height of the screen. - # (height changes as we loop over data_buffer, so remember the current value.) - # (Also make sure to clip the height to the size of the output.) - current_height = min(screen.height, height) - - # Loop over the rows. - row_count = min(max(screen.height, previous_screen.height), height) - c = 0 # Column counter. - - for y in range(row_count): - new_row = screen.data_buffer[y] - previous_row = previous_screen.data_buffer[y] - zero_width_escapes_row = screen.zero_width_escapes[y] - - new_max_line_len = min(width - 1, get_max_column_index(new_row)) - previous_max_line_len = min(width - 1, get_max_column_index(previous_row)) - - # Loop over the columns. - c = 0 - while c <= new_max_line_len: - new_char = new_row[c] - old_char = previous_row[c] - char_width = new_char.width or 1 - - # When the old and new character at this position are different, - # draw the output. (Because of the performance, we don't call - # `Char.__ne__`, but inline the same expression.) - if new_char.char != old_char.char or new_char.style != old_char.style: - current_pos = move_cursor(Point(x=c, y=y)) - - # Send injected escape sequences to output. - if c in zero_width_escapes_row: - write_raw(zero_width_escapes_row[c]) - - output_char(new_char) - current_pos = Point(x=current_pos.x + char_width, y=current_pos.y) - - c += char_width - - # If the new line is shorter, trim it. - if previous_screen and new_max_line_len < previous_max_line_len: - current_pos = move_cursor(Point(x=new_max_line_len + 1, y=y)) - reset_attributes() - output.erase_end_of_line() - - # Correctly reserve vertical space as required by the layout. - # When this is a new screen (drawn for the first time), or for some reason - # higher than the previous one. Move the cursor once to the bottom of the - # output. That way, we're sure that the terminal scrolls up, even when the - # lower lines of the canvas just contain whitespace. - - # The most obvious reason that we actually want this behaviour is the avoid - # the artifact of the input scrolling when the completion menu is shown. - # (If the scrolling is actually wanted, the layout can still be build in a - # way to behave that way by setting a dynamic height.) - if current_height > previous_screen.height: - current_pos = move_cursor(Point(x=0, y=current_height - 1)) - - # Move cursor: - if is_done: - current_pos = move_cursor(Point(x=0, y=current_height)) - output.erase_down() - else: - current_pos = move_cursor(screen.get_cursor_position(app.layout.current_window)) - - if is_done or not full_screen: - output.enable_autowrap() - - # Always reset the color attributes. This is important because a background - # thread could print data to stdout and we want that to be displayed in the - # default colors. (Also, if a background color has been set, many terminals - # give weird artifacts on resize events.) - reset_attributes() - - if screen.show_cursor or is_done: - output.show_cursor() - - return current_pos, last_style - - -class HeightIsUnknownError(Exception): - "Information unavailable. Did not yet receive the CPR response." - - -class _StyleStringToAttrsCache(Dict[str, Attrs]): - """ - A cache structure that maps style strings to :class:`.Attr`. - (This is an important speed up.) - """ - - def __init__( - self, - get_attrs_for_style_str: Callable[["str"], Attrs], - style_transformation: StyleTransformation, - ) -> None: - - self.get_attrs_for_style_str = get_attrs_for_style_str - self.style_transformation = style_transformation - - def __missing__(self, style_str: str) -> Attrs: - attrs = self.get_attrs_for_style_str(style_str) - attrs = self.style_transformation.transform_attrs(attrs) - - self[style_str] = attrs - return attrs - - -class _StyleStringHasStyleCache(Dict[str, bool]): - """ - Cache for remember which style strings don't render the default output - style (default fg/bg, no underline and no reverse and no blink). That way - we know that we should render these cells, even when they're empty (when - they contain a space). - - Note: we don't consider bold/italic/hidden because they don't change the - output if there's no text in the cell. - """ - - def __init__(self, style_string_to_attrs: Dict[str, Attrs]) -> None: - self.style_string_to_attrs = style_string_to_attrs - - def __missing__(self, style_str: str) -> bool: - attrs = self.style_string_to_attrs[style_str] - is_default = bool( - attrs.color - or attrs.bgcolor - or attrs.underline - or attrs.strike - or attrs.blink - or attrs.reverse - ) - - self[style_str] = is_default - return is_default - - -class CPR_Support(Enum): - "Enum: whether or not CPR is supported." - SUPPORTED = "SUPPORTED" - NOT_SUPPORTED = "NOT_SUPPORTED" - UNKNOWN = "UNKNOWN" - - -class Renderer: - """ - Typical usage: - - :: - - output = Vt100_Output.from_pty(sys.stdout) - r = Renderer(style, output) - r.render(app, layout=...) - """ - - CPR_TIMEOUT = 2 # Time to wait until we consider CPR to be not supported. - - def __init__( - self, - style: BaseStyle, - output: Output, - full_screen: bool = False, - mouse_support: FilterOrBool = False, - cpr_not_supported_callback: Optional[Callable[[], None]] = None, - ) -> None: - - self.style = style - self.output = output - self.full_screen = full_screen - self.mouse_support = to_filter(mouse_support) - self.cpr_not_supported_callback = cpr_not_supported_callback - - self._in_alternate_screen = False - self._mouse_support_enabled = False - self._bracketed_paste_enabled = False - self._cursor_key_mode_reset = False - - # Future set when we are waiting for a CPR flag. - self._waiting_for_cpr_futures: Deque[Future[None]] = deque() - self.cpr_support = CPR_Support.UNKNOWN - - if not output.responds_to_cpr: - self.cpr_support = CPR_Support.NOT_SUPPORTED - - # Cache for the style. - self._attrs_for_style: Optional[_StyleStringToAttrsCache] = None - self._style_string_has_style: Optional[_StyleStringHasStyleCache] = None - self._last_style_hash: Optional[Hashable] = None - self._last_transformation_hash: Optional[Hashable] = None - self._last_color_depth: Optional[ColorDepth] = None - - self.reset(_scroll=True) - - def reset(self, _scroll: bool = False, leave_alternate_screen: bool = True) -> None: - - # Reset position - self._cursor_pos = Point(x=0, y=0) - - # Remember the last screen instance between renderers. This way, - # we can create a `diff` between two screens and only output the - # difference. It's also to remember the last height. (To show for - # instance a toolbar at the bottom position.) - self._last_screen: Optional[Screen] = None - self._last_size: Optional[Size] = None - self._last_style: Optional[str] = None +from prompt_toolkit.data_structures import Point, Size +from prompt_toolkit.filters import FilterOrBool, to_filter +from prompt_toolkit.formatted_text import AnyFormattedText, to_formatted_text +from prompt_toolkit.layout.mouse_handlers import MouseHandlers +from prompt_toolkit.layout.screen import Char, Screen, WritePosition +from prompt_toolkit.output import ColorDepth, Output +from prompt_toolkit.styles import ( + Attrs, + BaseStyle, + DummyStyleTransformation, + StyleTransformation, +) + +if TYPE_CHECKING: + from prompt_toolkit.application import Application + from prompt_toolkit.layout.layout import Layout + + +__all__ = [ + "Renderer", + "print_formatted_text", +] + + +def _output_screen_diff( + app: "Application[Any]", + output: Output, + screen: Screen, + current_pos: Point, + color_depth: ColorDepth, + previous_screen: Optional[Screen], + last_style: Optional[str], + is_done: bool, # XXX: drop is_done + full_screen: bool, + attrs_for_style_string: "_StyleStringToAttrsCache", + style_string_has_style: "_StyleStringHasStyleCache", + size: Size, + previous_width: int, +) -> Tuple[Point, Optional[str]]: + """ + Render the diff between this screen and the previous screen. + + This takes two `Screen` instances. The one that represents the output like + it was during the last rendering and one that represents the current + output raster. Looking at these two `Screen` instances, this function will + render the difference by calling the appropriate methods of the `Output` + object that only paint the changes to the terminal. + + This is some performance-critical code which is heavily optimized. + Don't change things without profiling first. + + :param current_pos: Current cursor position. + :param last_style: The style string, used for drawing the last drawn + character. (Color/attributes.) + :param attrs_for_style_string: :class:`._StyleStringToAttrsCache` instance. + :param width: The width of the terminal. + :param previous_width: The width of the terminal during the last rendering. + """ + width, height = size.columns, size.rows + + #: Variable for capturing the output. + write = output.write + write_raw = output.write_raw + + # Create locals for the most used output methods. + # (Save expensive attribute lookups.) + _output_set_attributes = output.set_attributes + _output_reset_attributes = output.reset_attributes + _output_cursor_forward = output.cursor_forward + _output_cursor_up = output.cursor_up + _output_cursor_backward = output.cursor_backward + + # Hide cursor before rendering. (Avoid flickering.) + output.hide_cursor() + + def reset_attributes() -> None: + "Wrapper around Output.reset_attributes." + nonlocal last_style + _output_reset_attributes() + last_style = None # Forget last char after resetting attributes. + + def move_cursor(new: Point) -> Point: + "Move cursor to this `new` point. Returns the given Point." + current_x, current_y = current_pos.x, current_pos.y + + if new.y > current_y: + # Use newlines instead of CURSOR_DOWN, because this might add new lines. + # CURSOR_DOWN will never create new lines at the bottom. + # Also reset attributes, otherwise the newline could draw a + # background color. + reset_attributes() + write("\r\n" * (new.y - current_y)) + current_x = 0 + _output_cursor_forward(new.x) + return new + elif new.y < current_y: + _output_cursor_up(current_y - new.y) + + if current_x >= width - 1: + write("\r") + _output_cursor_forward(new.x) + elif new.x < current_x or current_x >= width - 1: + _output_cursor_backward(current_x - new.x) + elif new.x > current_x: + _output_cursor_forward(new.x - current_x) + + return new + + def output_char(char: Char) -> None: + """ + Write the output of this character. + """ + nonlocal last_style + + # If the last printed character has the same style, don't output the + # style again. + if last_style == char.style: + write(char.char) + else: + # Look up `Attr` for this style string. Only set attributes if different. + # (Two style strings can still have the same formatting.) + # Note that an empty style string can have formatting that needs to + # be applied, because of style transformations. + new_attrs = attrs_for_style_string[char.style] + if not last_style or new_attrs != attrs_for_style_string[last_style]: + _output_set_attributes(new_attrs, color_depth) + + write(char.char) + last_style = char.style + + def get_max_column_index(row: Dict[int, Char]) -> int: + """ + Return max used column index, ignoring whitespace (without style) at + the end of the line. This is important for people that copy/paste + terminal output. + + There are two reasons we are sometimes seeing whitespace at the end: + - `BufferControl` adds a trailing space to each line, because it's a + possible cursor position, so that the line wrapping won't change if + the cursor position moves around. + - The `Window` adds a style class to the current line for highlighting + (cursor-line). + """ + numbers = [ + index + for index, cell in row.items() + if cell.char != " " or style_string_has_style[cell.style] + ] + numbers.append(0) + return max(numbers) + + # Render for the first time: reset styling. + if not previous_screen: + reset_attributes() + + # Disable autowrap. (When entering a the alternate screen, or anytime when + # we have a prompt. - In the case of a REPL, like IPython, people can have + # background threads, and it's hard for debugging if their output is not + # wrapped.) + if not previous_screen or not full_screen: + output.disable_autowrap() + + # When the previous screen has a different size, redraw everything anyway. + # Also when we are done. (We might take up less rows, so clearing is important.) + if ( + is_done or not previous_screen or previous_width != width + ): # XXX: also consider height?? + current_pos = move_cursor(Point(x=0, y=0)) + reset_attributes() + output.erase_down() + + previous_screen = Screen() + + # Get height of the screen. + # (height changes as we loop over data_buffer, so remember the current value.) + # (Also make sure to clip the height to the size of the output.) + current_height = min(screen.height, height) + + # Loop over the rows. + row_count = min(max(screen.height, previous_screen.height), height) + c = 0 # Column counter. + + for y in range(row_count): + new_row = screen.data_buffer[y] + previous_row = previous_screen.data_buffer[y] + zero_width_escapes_row = screen.zero_width_escapes[y] + + new_max_line_len = min(width - 1, get_max_column_index(new_row)) + previous_max_line_len = min(width - 1, get_max_column_index(previous_row)) + + # Loop over the columns. + c = 0 + while c <= new_max_line_len: + new_char = new_row[c] + old_char = previous_row[c] + char_width = new_char.width or 1 + + # When the old and new character at this position are different, + # draw the output. (Because of the performance, we don't call + # `Char.__ne__`, but inline the same expression.) + if new_char.char != old_char.char or new_char.style != old_char.style: + current_pos = move_cursor(Point(x=c, y=y)) + + # Send injected escape sequences to output. + if c in zero_width_escapes_row: + write_raw(zero_width_escapes_row[c]) + + output_char(new_char) + current_pos = Point(x=current_pos.x + char_width, y=current_pos.y) + + c += char_width + + # If the new line is shorter, trim it. + if previous_screen and new_max_line_len < previous_max_line_len: + current_pos = move_cursor(Point(x=new_max_line_len + 1, y=y)) + reset_attributes() + output.erase_end_of_line() + + # Correctly reserve vertical space as required by the layout. + # When this is a new screen (drawn for the first time), or for some reason + # higher than the previous one. Move the cursor once to the bottom of the + # output. That way, we're sure that the terminal scrolls up, even when the + # lower lines of the canvas just contain whitespace. + + # The most obvious reason that we actually want this behaviour is the avoid + # the artifact of the input scrolling when the completion menu is shown. + # (If the scrolling is actually wanted, the layout can still be build in a + # way to behave that way by setting a dynamic height.) + if current_height > previous_screen.height: + current_pos = move_cursor(Point(x=0, y=current_height - 1)) + + # Move cursor: + if is_done: + current_pos = move_cursor(Point(x=0, y=current_height)) + output.erase_down() + else: + current_pos = move_cursor(screen.get_cursor_position(app.layout.current_window)) + + if is_done or not full_screen: + output.enable_autowrap() + + # Always reset the color attributes. This is important because a background + # thread could print data to stdout and we want that to be displayed in the + # default colors. (Also, if a background color has been set, many terminals + # give weird artifacts on resize events.) + reset_attributes() + + if screen.show_cursor or is_done: + output.show_cursor() + + return current_pos, last_style + + +class HeightIsUnknownError(Exception): + "Information unavailable. Did not yet receive the CPR response." + + +class _StyleStringToAttrsCache(Dict[str, Attrs]): + """ + A cache structure that maps style strings to :class:`.Attr`. + (This is an important speed up.) + """ + + def __init__( + self, + get_attrs_for_style_str: Callable[["str"], Attrs], + style_transformation: StyleTransformation, + ) -> None: + + self.get_attrs_for_style_str = get_attrs_for_style_str + self.style_transformation = style_transformation + + def __missing__(self, style_str: str) -> Attrs: + attrs = self.get_attrs_for_style_str(style_str) + attrs = self.style_transformation.transform_attrs(attrs) + + self[style_str] = attrs + return attrs + + +class _StyleStringHasStyleCache(Dict[str, bool]): + """ + Cache for remember which style strings don't render the default output + style (default fg/bg, no underline and no reverse and no blink). That way + we know that we should render these cells, even when they're empty (when + they contain a space). + + Note: we don't consider bold/italic/hidden because they don't change the + output if there's no text in the cell. + """ + + def __init__(self, style_string_to_attrs: Dict[str, Attrs]) -> None: + self.style_string_to_attrs = style_string_to_attrs + + def __missing__(self, style_str: str) -> bool: + attrs = self.style_string_to_attrs[style_str] + is_default = bool( + attrs.color + or attrs.bgcolor + or attrs.underline + or attrs.strike + or attrs.blink + or attrs.reverse + ) + + self[style_str] = is_default + return is_default + + +class CPR_Support(Enum): + "Enum: whether or not CPR is supported." + SUPPORTED = "SUPPORTED" + NOT_SUPPORTED = "NOT_SUPPORTED" + UNKNOWN = "UNKNOWN" + + +class Renderer: + """ + Typical usage: + + :: + + output = Vt100_Output.from_pty(sys.stdout) + r = Renderer(style, output) + r.render(app, layout=...) + """ + + CPR_TIMEOUT = 2 # Time to wait until we consider CPR to be not supported. + + def __init__( + self, + style: BaseStyle, + output: Output, + full_screen: bool = False, + mouse_support: FilterOrBool = False, + cpr_not_supported_callback: Optional[Callable[[], None]] = None, + ) -> None: + + self.style = style + self.output = output + self.full_screen = full_screen + self.mouse_support = to_filter(mouse_support) + self.cpr_not_supported_callback = cpr_not_supported_callback + + self._in_alternate_screen = False + self._mouse_support_enabled = False + self._bracketed_paste_enabled = False + self._cursor_key_mode_reset = False + + # Future set when we are waiting for a CPR flag. + self._waiting_for_cpr_futures: Deque[Future[None]] = deque() + self.cpr_support = CPR_Support.UNKNOWN + + if not output.responds_to_cpr: + self.cpr_support = CPR_Support.NOT_SUPPORTED + + # Cache for the style. + self._attrs_for_style: Optional[_StyleStringToAttrsCache] = None + self._style_string_has_style: Optional[_StyleStringHasStyleCache] = None + self._last_style_hash: Optional[Hashable] = None + self._last_transformation_hash: Optional[Hashable] = None + self._last_color_depth: Optional[ColorDepth] = None + + self.reset(_scroll=True) + + def reset(self, _scroll: bool = False, leave_alternate_screen: bool = True) -> None: + + # Reset position + self._cursor_pos = Point(x=0, y=0) + + # Remember the last screen instance between renderers. This way, + # we can create a `diff` between two screens and only output the + # difference. It's also to remember the last height. (To show for + # instance a toolbar at the bottom position.) + self._last_screen: Optional[Screen] = None + self._last_size: Optional[Size] = None + self._last_style: Optional[str] = None self._last_cursor_shape: Optional[CursorShape] = None - - # Default MouseHandlers. (Just empty.) - self.mouse_handlers = MouseHandlers() - - #: Space from the top of the layout, until the bottom of the terminal. - #: We don't know this until a `report_absolute_cursor_row` call. - self._min_available_height = 0 - - # In case of Windows, also make sure to scroll to the current cursor - # position. (Only when rendering the first time.) - # It does nothing for vt100 terminals. - if _scroll: - self.output.scroll_buffer_to_prompt() - - # Quit alternate screen. - if self._in_alternate_screen and leave_alternate_screen: - self.output.quit_alternate_screen() - self._in_alternate_screen = False - - # Disable mouse support. - if self._mouse_support_enabled: - self.output.disable_mouse_support() - self._mouse_support_enabled = False - - # Disable bracketed paste. - if self._bracketed_paste_enabled: - self.output.disable_bracketed_paste() - self._bracketed_paste_enabled = False - - # NOTE: No need to set/reset cursor key mode here. - - # Flush output. `disable_mouse_support` needs to write to stdout. - self.output.flush() - - @property - def last_rendered_screen(self) -> Optional[Screen]: - """ - The `Screen` class that was generated during the last rendering. - This can be `None`. - """ - return self._last_screen - - @property - def height_is_known(self) -> bool: - """ - True when the height from the cursor until the bottom of the terminal - is known. (It's often nicer to draw bottom toolbars only if the height - is known, in order to avoid flickering when the CPR response arrives.) - """ - if self.full_screen or self._min_available_height > 0: - return True - try: - self._min_available_height = self.output.get_rows_below_cursor_position() - return True - except NotImplementedError: - return False - - @property - def rows_above_layout(self) -> int: - """ - Return the number of rows visible in the terminal above the layout. - """ - if self._in_alternate_screen: - return 0 - elif self._min_available_height > 0: - total_rows = self.output.get_size().rows - last_screen_height = self._last_screen.height if self._last_screen else 0 - return total_rows - max(self._min_available_height, last_screen_height) - else: - raise HeightIsUnknownError("Rows above layout is unknown.") - - def request_absolute_cursor_position(self) -> None: - """ - Get current cursor position. - - We do this to calculate the minimum available height that we can - consume for rendering the prompt. This is the available space below te - cursor. - - For vt100: Do CPR request. (answer will arrive later.) - For win32: Do API call. (Answer comes immediately.) - """ - # Only do this request when the cursor is at the top row. (after a - # clear or reset). We will rely on that in `report_absolute_cursor_row`. - assert self._cursor_pos.y == 0 - - # In full-screen mode, always use the total height as min-available-height. - if self.full_screen: - self._min_available_height = self.output.get_size().rows - return - - # For Win32, we have an API call to get the number of rows below the - # cursor. - try: - self._min_available_height = self.output.get_rows_below_cursor_position() - return - except NotImplementedError: - pass - - # Use CPR. - if self.cpr_support == CPR_Support.NOT_SUPPORTED: - return - - def do_cpr() -> None: - # Asks for a cursor position report (CPR). - self._waiting_for_cpr_futures.append(Future()) - self.output.ask_for_cpr() - - if self.cpr_support == CPR_Support.SUPPORTED: - do_cpr() - return - - # If we don't know whether CPR is supported, only do a request if - # none is pending, and test it, using a timer. - if self.waiting_for_cpr: - return - - do_cpr() - - async def timer() -> None: - await sleep(self.CPR_TIMEOUT) - - # Not set in the meantime -> not supported. - if self.cpr_support == CPR_Support.UNKNOWN: - self.cpr_support = CPR_Support.NOT_SUPPORTED - - if self.cpr_not_supported_callback: - # Make sure to call this callback in the main thread. - self.cpr_not_supported_callback() - - get_app().create_background_task(timer()) - - def report_absolute_cursor_row(self, row: int) -> None: - """ - To be called when we know the absolute cursor position. - (As an answer of a "Cursor Position Request" response.) - """ - self.cpr_support = CPR_Support.SUPPORTED - - # Calculate the amount of rows from the cursor position until the - # bottom of the terminal. - total_rows = self.output.get_size().rows - rows_below_cursor = total_rows - row + 1 - - # Set the minimum available height. - self._min_available_height = rows_below_cursor - - # Pop and set waiting for CPR future. - try: - f = self._waiting_for_cpr_futures.popleft() - except IndexError: - pass # Received CPR response without having a CPR. - else: - f.set_result(None) - - @property - def waiting_for_cpr(self) -> bool: - """ - Waiting for CPR flag. True when we send the request, but didn't got a - response. - """ - return bool(self._waiting_for_cpr_futures) - - async def wait_for_cpr_responses(self, timeout: int = 1) -> None: - """ - Wait for a CPR response. - """ - cpr_futures = list(self._waiting_for_cpr_futures) # Make copy. - - # When there are no CPRs in the queue. Don't do anything. - if not cpr_futures or self.cpr_support == CPR_Support.NOT_SUPPORTED: - return None - - async def wait_for_responses() -> None: - for response_f in cpr_futures: - await response_f - - async def wait_for_timeout() -> None: - await sleep(timeout) - - # Got timeout, erase queue. - for response_f in cpr_futures: - response_f.cancel() - self._waiting_for_cpr_futures = deque() - - tasks = { - ensure_future(wait_for_responses()), - ensure_future(wait_for_timeout()), - } - _, pending = await wait(tasks, return_when=FIRST_COMPLETED) - for task in pending: - task.cancel() - - def render( - self, app: "Application[Any]", layout: "Layout", is_done: bool = False - ) -> None: - """ - Render the current interface to the output. - - :param is_done: When True, put the cursor at the end of the interface. We - won't print any changes to this part. - """ - output = self.output - - # Enter alternate screen. - if self.full_screen and not self._in_alternate_screen: - self._in_alternate_screen = True - output.enter_alternate_screen() - - # Enable bracketed paste. - if not self._bracketed_paste_enabled: - self.output.enable_bracketed_paste() - self._bracketed_paste_enabled = True - - # Reset cursor key mode. - if not self._cursor_key_mode_reset: - self.output.reset_cursor_key_mode() - self._cursor_key_mode_reset = True - - # Enable/disable mouse support. - needs_mouse_support = self.mouse_support() - - if needs_mouse_support and not self._mouse_support_enabled: - output.enable_mouse_support() - self._mouse_support_enabled = True - - elif not needs_mouse_support and self._mouse_support_enabled: - output.disable_mouse_support() - self._mouse_support_enabled = False - - # Create screen and write layout to it. - size = output.get_size() - screen = Screen() - screen.show_cursor = False # Hide cursor by default, unless one of the - # containers decides to display it. - mouse_handlers = MouseHandlers() - - # Calculate height. - if self.full_screen: - height = size.rows - elif is_done: - # When we are done, we don't necessary want to fill up until the bottom. - height = layout.container.preferred_height( - size.columns, size.rows - ).preferred - else: - last_height = self._last_screen.height if self._last_screen else 0 - height = max( - self._min_available_height, - last_height, - layout.container.preferred_height(size.columns, size.rows).preferred, - ) - - height = min(height, size.rows) - - # When the size changes, don't consider the previous screen. - if self._last_size != size: - self._last_screen = None - - # When we render using another style or another color depth, do a full - # repaint. (Forget about the previous rendered screen.) - # (But note that we still use _last_screen to calculate the height.) - if ( - self.style.invalidation_hash() != self._last_style_hash - or app.style_transformation.invalidation_hash() - != self._last_transformation_hash - or app.color_depth != self._last_color_depth - ): - self._last_screen = None - self._attrs_for_style = None - self._style_string_has_style = None - - if self._attrs_for_style is None: - self._attrs_for_style = _StyleStringToAttrsCache( - self.style.get_attrs_for_style_str, app.style_transformation - ) - if self._style_string_has_style is None: - self._style_string_has_style = _StyleStringHasStyleCache( - self._attrs_for_style - ) - - self._last_style_hash = self.style.invalidation_hash() - self._last_transformation_hash = app.style_transformation.invalidation_hash() - self._last_color_depth = app.color_depth - - layout.container.write_to_screen( - screen, - mouse_handlers, - WritePosition(xpos=0, ypos=0, width=size.columns, height=height), - parent_style="", - erase_bg=False, - z_index=None, - ) - screen.draw_all_floats() - - # When grayed. Replace all styles in the new screen. - if app.exit_style: - screen.append_style_to_content(app.exit_style) - - # Process diff and write to output. - self._cursor_pos, self._last_style = _output_screen_diff( - app, - output, - screen, - self._cursor_pos, - app.color_depth, - self._last_screen, - self._last_style, - is_done, - full_screen=self.full_screen, - attrs_for_style_string=self._attrs_for_style, - style_string_has_style=self._style_string_has_style, - size=size, - previous_width=(self._last_size.columns if self._last_size else 0), - ) - self._last_screen = screen - self._last_size = size - self.mouse_handlers = mouse_handlers - + + # Default MouseHandlers. (Just empty.) + self.mouse_handlers = MouseHandlers() + + #: Space from the top of the layout, until the bottom of the terminal. + #: We don't know this until a `report_absolute_cursor_row` call. + self._min_available_height = 0 + + # In case of Windows, also make sure to scroll to the current cursor + # position. (Only when rendering the first time.) + # It does nothing for vt100 terminals. + if _scroll: + self.output.scroll_buffer_to_prompt() + + # Quit alternate screen. + if self._in_alternate_screen and leave_alternate_screen: + self.output.quit_alternate_screen() + self._in_alternate_screen = False + + # Disable mouse support. + if self._mouse_support_enabled: + self.output.disable_mouse_support() + self._mouse_support_enabled = False + + # Disable bracketed paste. + if self._bracketed_paste_enabled: + self.output.disable_bracketed_paste() + self._bracketed_paste_enabled = False + + # NOTE: No need to set/reset cursor key mode here. + + # Flush output. `disable_mouse_support` needs to write to stdout. + self.output.flush() + + @property + def last_rendered_screen(self) -> Optional[Screen]: + """ + The `Screen` class that was generated during the last rendering. + This can be `None`. + """ + return self._last_screen + + @property + def height_is_known(self) -> bool: + """ + True when the height from the cursor until the bottom of the terminal + is known. (It's often nicer to draw bottom toolbars only if the height + is known, in order to avoid flickering when the CPR response arrives.) + """ + if self.full_screen or self._min_available_height > 0: + return True + try: + self._min_available_height = self.output.get_rows_below_cursor_position() + return True + except NotImplementedError: + return False + + @property + def rows_above_layout(self) -> int: + """ + Return the number of rows visible in the terminal above the layout. + """ + if self._in_alternate_screen: + return 0 + elif self._min_available_height > 0: + total_rows = self.output.get_size().rows + last_screen_height = self._last_screen.height if self._last_screen else 0 + return total_rows - max(self._min_available_height, last_screen_height) + else: + raise HeightIsUnknownError("Rows above layout is unknown.") + + def request_absolute_cursor_position(self) -> None: + """ + Get current cursor position. + + We do this to calculate the minimum available height that we can + consume for rendering the prompt. This is the available space below te + cursor. + + For vt100: Do CPR request. (answer will arrive later.) + For win32: Do API call. (Answer comes immediately.) + """ + # Only do this request when the cursor is at the top row. (after a + # clear or reset). We will rely on that in `report_absolute_cursor_row`. + assert self._cursor_pos.y == 0 + + # In full-screen mode, always use the total height as min-available-height. + if self.full_screen: + self._min_available_height = self.output.get_size().rows + return + + # For Win32, we have an API call to get the number of rows below the + # cursor. + try: + self._min_available_height = self.output.get_rows_below_cursor_position() + return + except NotImplementedError: + pass + + # Use CPR. + if self.cpr_support == CPR_Support.NOT_SUPPORTED: + return + + def do_cpr() -> None: + # Asks for a cursor position report (CPR). + self._waiting_for_cpr_futures.append(Future()) + self.output.ask_for_cpr() + + if self.cpr_support == CPR_Support.SUPPORTED: + do_cpr() + return + + # If we don't know whether CPR is supported, only do a request if + # none is pending, and test it, using a timer. + if self.waiting_for_cpr: + return + + do_cpr() + + async def timer() -> None: + await sleep(self.CPR_TIMEOUT) + + # Not set in the meantime -> not supported. + if self.cpr_support == CPR_Support.UNKNOWN: + self.cpr_support = CPR_Support.NOT_SUPPORTED + + if self.cpr_not_supported_callback: + # Make sure to call this callback in the main thread. + self.cpr_not_supported_callback() + + get_app().create_background_task(timer()) + + def report_absolute_cursor_row(self, row: int) -> None: + """ + To be called when we know the absolute cursor position. + (As an answer of a "Cursor Position Request" response.) + """ + self.cpr_support = CPR_Support.SUPPORTED + + # Calculate the amount of rows from the cursor position until the + # bottom of the terminal. + total_rows = self.output.get_size().rows + rows_below_cursor = total_rows - row + 1 + + # Set the minimum available height. + self._min_available_height = rows_below_cursor + + # Pop and set waiting for CPR future. + try: + f = self._waiting_for_cpr_futures.popleft() + except IndexError: + pass # Received CPR response without having a CPR. + else: + f.set_result(None) + + @property + def waiting_for_cpr(self) -> bool: + """ + Waiting for CPR flag. True when we send the request, but didn't got a + response. + """ + return bool(self._waiting_for_cpr_futures) + + async def wait_for_cpr_responses(self, timeout: int = 1) -> None: + """ + Wait for a CPR response. + """ + cpr_futures = list(self._waiting_for_cpr_futures) # Make copy. + + # When there are no CPRs in the queue. Don't do anything. + if not cpr_futures or self.cpr_support == CPR_Support.NOT_SUPPORTED: + return None + + async def wait_for_responses() -> None: + for response_f in cpr_futures: + await response_f + + async def wait_for_timeout() -> None: + await sleep(timeout) + + # Got timeout, erase queue. + for response_f in cpr_futures: + response_f.cancel() + self._waiting_for_cpr_futures = deque() + + tasks = { + ensure_future(wait_for_responses()), + ensure_future(wait_for_timeout()), + } + _, pending = await wait(tasks, return_when=FIRST_COMPLETED) + for task in pending: + task.cancel() + + def render( + self, app: "Application[Any]", layout: "Layout", is_done: bool = False + ) -> None: + """ + Render the current interface to the output. + + :param is_done: When True, put the cursor at the end of the interface. We + won't print any changes to this part. + """ + output = self.output + + # Enter alternate screen. + if self.full_screen and not self._in_alternate_screen: + self._in_alternate_screen = True + output.enter_alternate_screen() + + # Enable bracketed paste. + if not self._bracketed_paste_enabled: + self.output.enable_bracketed_paste() + self._bracketed_paste_enabled = True + + # Reset cursor key mode. + if not self._cursor_key_mode_reset: + self.output.reset_cursor_key_mode() + self._cursor_key_mode_reset = True + + # Enable/disable mouse support. + needs_mouse_support = self.mouse_support() + + if needs_mouse_support and not self._mouse_support_enabled: + output.enable_mouse_support() + self._mouse_support_enabled = True + + elif not needs_mouse_support and self._mouse_support_enabled: + output.disable_mouse_support() + self._mouse_support_enabled = False + + # Create screen and write layout to it. + size = output.get_size() + screen = Screen() + screen.show_cursor = False # Hide cursor by default, unless one of the + # containers decides to display it. + mouse_handlers = MouseHandlers() + + # Calculate height. + if self.full_screen: + height = size.rows + elif is_done: + # When we are done, we don't necessary want to fill up until the bottom. + height = layout.container.preferred_height( + size.columns, size.rows + ).preferred + else: + last_height = self._last_screen.height if self._last_screen else 0 + height = max( + self._min_available_height, + last_height, + layout.container.preferred_height(size.columns, size.rows).preferred, + ) + + height = min(height, size.rows) + + # When the size changes, don't consider the previous screen. + if self._last_size != size: + self._last_screen = None + + # When we render using another style or another color depth, do a full + # repaint. (Forget about the previous rendered screen.) + # (But note that we still use _last_screen to calculate the height.) + if ( + self.style.invalidation_hash() != self._last_style_hash + or app.style_transformation.invalidation_hash() + != self._last_transformation_hash + or app.color_depth != self._last_color_depth + ): + self._last_screen = None + self._attrs_for_style = None + self._style_string_has_style = None + + if self._attrs_for_style is None: + self._attrs_for_style = _StyleStringToAttrsCache( + self.style.get_attrs_for_style_str, app.style_transformation + ) + if self._style_string_has_style is None: + self._style_string_has_style = _StyleStringHasStyleCache( + self._attrs_for_style + ) + + self._last_style_hash = self.style.invalidation_hash() + self._last_transformation_hash = app.style_transformation.invalidation_hash() + self._last_color_depth = app.color_depth + + layout.container.write_to_screen( + screen, + mouse_handlers, + WritePosition(xpos=0, ypos=0, width=size.columns, height=height), + parent_style="", + erase_bg=False, + z_index=None, + ) + screen.draw_all_floats() + + # When grayed. Replace all styles in the new screen. + if app.exit_style: + screen.append_style_to_content(app.exit_style) + + # Process diff and write to output. + self._cursor_pos, self._last_style = _output_screen_diff( + app, + output, + screen, + self._cursor_pos, + app.color_depth, + self._last_screen, + self._last_style, + is_done, + full_screen=self.full_screen, + attrs_for_style_string=self._attrs_for_style, + style_string_has_style=self._style_string_has_style, + size=size, + previous_width=(self._last_size.columns if self._last_size else 0), + ) + self._last_screen = screen + self._last_size = size + self.mouse_handlers = mouse_handlers + # Handle cursor shapes. new_cursor_shape = app.cursor.get_cursor_shape(app) if ( @@ -716,95 +716,95 @@ class Renderer: self._last_cursor_shape = new_cursor_shape # Flush buffered output. - output.flush() - - # Set visible windows in layout. - app.layout.visible_windows = screen.visible_windows - - if is_done: - self.reset() - - def erase(self, leave_alternate_screen: bool = True) -> None: - """ - Hide all output and put the cursor back at the first line. This is for - instance used for running a system command (while hiding the CLI) and - later resuming the same CLI.) - - :param leave_alternate_screen: When True, and when inside an alternate - screen buffer, quit the alternate screen. - """ - output = self.output - - output.cursor_backward(self._cursor_pos.x) - output.cursor_up(self._cursor_pos.y) - output.erase_down() - output.reset_attributes() - output.enable_autowrap() + output.flush() + + # Set visible windows in layout. + app.layout.visible_windows = screen.visible_windows + + if is_done: + self.reset() + + def erase(self, leave_alternate_screen: bool = True) -> None: + """ + Hide all output and put the cursor back at the first line. This is for + instance used for running a system command (while hiding the CLI) and + later resuming the same CLI.) + + :param leave_alternate_screen: When True, and when inside an alternate + screen buffer, quit the alternate screen. + """ + output = self.output + + output.cursor_backward(self._cursor_pos.x) + output.cursor_up(self._cursor_pos.y) + output.erase_down() + output.reset_attributes() + output.enable_autowrap() output.reset_cursor_shape() - output.flush() - - self.reset(leave_alternate_screen=leave_alternate_screen) - - def clear(self) -> None: - """ - Clear screen and go to 0,0 - """ - # Erase current output first. - self.erase() - - # Send "Erase Screen" command and go to (0, 0). - output = self.output - - output.erase_screen() - output.cursor_goto(0, 0) - output.flush() - - self.request_absolute_cursor_position() - - -def print_formatted_text( - output: Output, - formatted_text: AnyFormattedText, - style: BaseStyle, - style_transformation: Optional[StyleTransformation] = None, - color_depth: Optional[ColorDepth] = None, -) -> None: - """ - Print a list of (style_str, text) tuples in the given style to the output. - """ - fragments = to_formatted_text(formatted_text) - style_transformation = style_transformation or DummyStyleTransformation() - color_depth = color_depth or output.get_default_color_depth() - - # Reset first. - output.reset_attributes() - output.enable_autowrap() - last_attrs: Optional[Attrs] = None - - # Print all (style_str, text) tuples. - attrs_for_style_string = _StyleStringToAttrsCache( - style.get_attrs_for_style_str, style_transformation - ) - - for style_str, text, *_ in fragments: - attrs = attrs_for_style_string[style_str] - - # Set style attributes if something changed. - if attrs != last_attrs: - if attrs: - output.set_attributes(attrs, color_depth) - else: - output.reset_attributes() - last_attrs = attrs - - # Eliminate carriage returns - text = text.replace("\r", "") - - # Assume that the output is raw, and insert a carriage return before - # every newline. (Also important when the front-end is a telnet client.) - output.write(text.replace("\n", "\r\n")) - - # Reset again. - output.reset_attributes() - output.flush() + output.flush() + + self.reset(leave_alternate_screen=leave_alternate_screen) + + def clear(self) -> None: + """ + Clear screen and go to 0,0 + """ + # Erase current output first. + self.erase() + + # Send "Erase Screen" command and go to (0, 0). + output = self.output + + output.erase_screen() + output.cursor_goto(0, 0) + output.flush() + + self.request_absolute_cursor_position() + + +def print_formatted_text( + output: Output, + formatted_text: AnyFormattedText, + style: BaseStyle, + style_transformation: Optional[StyleTransformation] = None, + color_depth: Optional[ColorDepth] = None, +) -> None: + """ + Print a list of (style_str, text) tuples in the given style to the output. + """ + fragments = to_formatted_text(formatted_text) + style_transformation = style_transformation or DummyStyleTransformation() + color_depth = color_depth or output.get_default_color_depth() + + # Reset first. + output.reset_attributes() + output.enable_autowrap() + last_attrs: Optional[Attrs] = None + + # Print all (style_str, text) tuples. + attrs_for_style_string = _StyleStringToAttrsCache( + style.get_attrs_for_style_str, style_transformation + ) + + for style_str, text, *_ in fragments: + attrs = attrs_for_style_string[style_str] + + # Set style attributes if something changed. + if attrs != last_attrs: + if attrs: + output.set_attributes(attrs, color_depth) + else: + output.reset_attributes() + last_attrs = attrs + + # Eliminate carriage returns + text = text.replace("\r", "") + + # Assume that the output is raw, and insert a carriage return before + # every newline. (Also important when the front-end is a telnet client.) + output.write(text.replace("\n", "\r\n")) + + # Reset again. + output.reset_attributes() + output.flush() diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/search.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/search.py index 1d6c4764df..6f94afe924 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/search.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/search.py @@ -1,229 +1,229 @@ -""" -Search operations. - -For the key bindings implementation with attached filters, check -`prompt_toolkit.key_binding.bindings.search`. (Use these for new key bindings -instead of calling these function directly.) -""" -from enum import Enum -from typing import TYPE_CHECKING, Dict, Optional - -from .application.current import get_app -from .filters import FilterOrBool, is_searching, to_filter -from .key_binding.vi_state import InputMode - -if TYPE_CHECKING: - from prompt_toolkit.layout.controls import BufferControl, SearchBufferControl - from prompt_toolkit.layout.layout import Layout - -__all__ = [ - "SearchDirection", - "start_search", - "stop_search", -] - - -class SearchDirection(Enum): - FORWARD = "FORWARD" - BACKWARD = "BACKWARD" - - -class SearchState: - """ - A search 'query', associated with a search field (like a SearchToolbar). - - Every searchable `BufferControl` points to a `search_buffer_control` - (another `BufferControls`) which represents the search field. The - `SearchState` attached to that search field is used for storing the current - search query. - - It is possible to have one searchfield for multiple `BufferControls`. In - that case, they'll share the same `SearchState`. - If there are multiple `BufferControls` that display the same `Buffer`, then - they can have a different `SearchState` each (if they have a different - search control). - """ - - __slots__ = ("text", "direction", "ignore_case") - - def __init__( - self, - text: str = "", - direction: SearchDirection = SearchDirection.FORWARD, - ignore_case: FilterOrBool = False, - ) -> None: - - self.text = text - self.direction = direction - self.ignore_case = to_filter(ignore_case) - - def __repr__(self) -> str: - return "%s(%r, direction=%r, ignore_case=%r)" % ( - self.__class__.__name__, - self.text, - self.direction, - self.ignore_case, - ) - - def __invert__(self) -> "SearchState": - """ - Create a new SearchState where backwards becomes forwards and the other - way around. - """ - if self.direction == SearchDirection.BACKWARD: - direction = SearchDirection.FORWARD - else: - direction = SearchDirection.BACKWARD - - return SearchState( - text=self.text, direction=direction, ignore_case=self.ignore_case - ) - - -def start_search( - buffer_control: Optional["BufferControl"] = None, - direction: SearchDirection = SearchDirection.FORWARD, -) -> None: - """ - Start search through the given `buffer_control` using the - `search_buffer_control`. - - :param buffer_control: Start search for this `BufferControl`. If not given, - search through the current control. - """ - from prompt_toolkit.layout.controls import BufferControl - - assert buffer_control is None or isinstance(buffer_control, BufferControl) - - layout = get_app().layout - - # When no control is given, use the current control if that's a BufferControl. - if buffer_control is None: - if not isinstance(layout.current_control, BufferControl): - return - buffer_control = layout.current_control - - # Only if this control is searchable. - search_buffer_control = buffer_control.search_buffer_control - - if search_buffer_control: - buffer_control.search_state.direction = direction - - # Make sure to focus the search BufferControl - layout.focus(search_buffer_control) - - # Remember search link. - layout.search_links[search_buffer_control] = buffer_control - - # If we're in Vi mode, make sure to go into insert mode. - get_app().vi_state.input_mode = InputMode.INSERT - - -def stop_search(buffer_control: Optional["BufferControl"] = None) -> None: - """ - Stop search through the given `buffer_control`. - """ - layout = get_app().layout - - if buffer_control is None: - buffer_control = layout.search_target_buffer_control - if buffer_control is None: - # (Should not happen, but possible when `stop_search` is called - # when we're not searching.) - return - search_buffer_control = buffer_control.search_buffer_control - else: - assert buffer_control in layout.search_links.values() - search_buffer_control = _get_reverse_search_links(layout)[buffer_control] - - # Focus the original buffer again. - layout.focus(buffer_control) - - if search_buffer_control is not None: - # Remove the search link. - del layout.search_links[search_buffer_control] - - # Reset content of search control. - search_buffer_control.buffer.reset() - - # If we're in Vi mode, go back to navigation mode. - get_app().vi_state.input_mode = InputMode.NAVIGATION - - -def do_incremental_search(direction: SearchDirection, count: int = 1) -> None: - """ - Apply search, but keep search buffer focused. - """ - assert is_searching() - - layout = get_app().layout - - # Only search if the current control is a `BufferControl`. - from prompt_toolkit.layout.controls import BufferControl - - search_control = layout.current_control - if not isinstance(search_control, BufferControl): - return - - prev_control = layout.search_target_buffer_control - if prev_control is None: - return - search_state = prev_control.search_state - - # Update search_state. - direction_changed = search_state.direction != direction - - search_state.text = search_control.buffer.text - search_state.direction = direction - - # Apply search to current buffer. - if not direction_changed: - prev_control.buffer.apply_search( - search_state, include_current_position=False, count=count - ) - - -def accept_search() -> None: - """ - Accept current search query. Focus original `BufferControl` again. - """ - layout = get_app().layout - - search_control = layout.current_control - target_buffer_control = layout.search_target_buffer_control - - from prompt_toolkit.layout.controls import BufferControl - - if not isinstance(search_control, BufferControl): - return - if target_buffer_control is None: - return - - search_state = target_buffer_control.search_state - - # Update search state. - if search_control.buffer.text: - search_state.text = search_control.buffer.text - - # Apply search. - target_buffer_control.buffer.apply_search( - search_state, include_current_position=True - ) - - # Add query to history of search line. - search_control.buffer.append_to_history() - - # Stop search and focus previous control again. - stop_search(target_buffer_control) - - -def _get_reverse_search_links( - layout: "Layout", -) -> Dict["BufferControl", "SearchBufferControl"]: - """ - Return mapping from BufferControl to SearchBufferControl. - """ - return { - buffer_control: search_buffer_control - for search_buffer_control, buffer_control in layout.search_links.items() - } +""" +Search operations. + +For the key bindings implementation with attached filters, check +`prompt_toolkit.key_binding.bindings.search`. (Use these for new key bindings +instead of calling these function directly.) +""" +from enum import Enum +from typing import TYPE_CHECKING, Dict, Optional + +from .application.current import get_app +from .filters import FilterOrBool, is_searching, to_filter +from .key_binding.vi_state import InputMode + +if TYPE_CHECKING: + from prompt_toolkit.layout.controls import BufferControl, SearchBufferControl + from prompt_toolkit.layout.layout import Layout + +__all__ = [ + "SearchDirection", + "start_search", + "stop_search", +] + + +class SearchDirection(Enum): + FORWARD = "FORWARD" + BACKWARD = "BACKWARD" + + +class SearchState: + """ + A search 'query', associated with a search field (like a SearchToolbar). + + Every searchable `BufferControl` points to a `search_buffer_control` + (another `BufferControls`) which represents the search field. The + `SearchState` attached to that search field is used for storing the current + search query. + + It is possible to have one searchfield for multiple `BufferControls`. In + that case, they'll share the same `SearchState`. + If there are multiple `BufferControls` that display the same `Buffer`, then + they can have a different `SearchState` each (if they have a different + search control). + """ + + __slots__ = ("text", "direction", "ignore_case") + + def __init__( + self, + text: str = "", + direction: SearchDirection = SearchDirection.FORWARD, + ignore_case: FilterOrBool = False, + ) -> None: + + self.text = text + self.direction = direction + self.ignore_case = to_filter(ignore_case) + + def __repr__(self) -> str: + return "%s(%r, direction=%r, ignore_case=%r)" % ( + self.__class__.__name__, + self.text, + self.direction, + self.ignore_case, + ) + + def __invert__(self) -> "SearchState": + """ + Create a new SearchState where backwards becomes forwards and the other + way around. + """ + if self.direction == SearchDirection.BACKWARD: + direction = SearchDirection.FORWARD + else: + direction = SearchDirection.BACKWARD + + return SearchState( + text=self.text, direction=direction, ignore_case=self.ignore_case + ) + + +def start_search( + buffer_control: Optional["BufferControl"] = None, + direction: SearchDirection = SearchDirection.FORWARD, +) -> None: + """ + Start search through the given `buffer_control` using the + `search_buffer_control`. + + :param buffer_control: Start search for this `BufferControl`. If not given, + search through the current control. + """ + from prompt_toolkit.layout.controls import BufferControl + + assert buffer_control is None or isinstance(buffer_control, BufferControl) + + layout = get_app().layout + + # When no control is given, use the current control if that's a BufferControl. + if buffer_control is None: + if not isinstance(layout.current_control, BufferControl): + return + buffer_control = layout.current_control + + # Only if this control is searchable. + search_buffer_control = buffer_control.search_buffer_control + + if search_buffer_control: + buffer_control.search_state.direction = direction + + # Make sure to focus the search BufferControl + layout.focus(search_buffer_control) + + # Remember search link. + layout.search_links[search_buffer_control] = buffer_control + + # If we're in Vi mode, make sure to go into insert mode. + get_app().vi_state.input_mode = InputMode.INSERT + + +def stop_search(buffer_control: Optional["BufferControl"] = None) -> None: + """ + Stop search through the given `buffer_control`. + """ + layout = get_app().layout + + if buffer_control is None: + buffer_control = layout.search_target_buffer_control + if buffer_control is None: + # (Should not happen, but possible when `stop_search` is called + # when we're not searching.) + return + search_buffer_control = buffer_control.search_buffer_control + else: + assert buffer_control in layout.search_links.values() + search_buffer_control = _get_reverse_search_links(layout)[buffer_control] + + # Focus the original buffer again. + layout.focus(buffer_control) + + if search_buffer_control is not None: + # Remove the search link. + del layout.search_links[search_buffer_control] + + # Reset content of search control. + search_buffer_control.buffer.reset() + + # If we're in Vi mode, go back to navigation mode. + get_app().vi_state.input_mode = InputMode.NAVIGATION + + +def do_incremental_search(direction: SearchDirection, count: int = 1) -> None: + """ + Apply search, but keep search buffer focused. + """ + assert is_searching() + + layout = get_app().layout + + # Only search if the current control is a `BufferControl`. + from prompt_toolkit.layout.controls import BufferControl + + search_control = layout.current_control + if not isinstance(search_control, BufferControl): + return + + prev_control = layout.search_target_buffer_control + if prev_control is None: + return + search_state = prev_control.search_state + + # Update search_state. + direction_changed = search_state.direction != direction + + search_state.text = search_control.buffer.text + search_state.direction = direction + + # Apply search to current buffer. + if not direction_changed: + prev_control.buffer.apply_search( + search_state, include_current_position=False, count=count + ) + + +def accept_search() -> None: + """ + Accept current search query. Focus original `BufferControl` again. + """ + layout = get_app().layout + + search_control = layout.current_control + target_buffer_control = layout.search_target_buffer_control + + from prompt_toolkit.layout.controls import BufferControl + + if not isinstance(search_control, BufferControl): + return + if target_buffer_control is None: + return + + search_state = target_buffer_control.search_state + + # Update search state. + if search_control.buffer.text: + search_state.text = search_control.buffer.text + + # Apply search. + target_buffer_control.buffer.apply_search( + search_state, include_current_position=True + ) + + # Add query to history of search line. + search_control.buffer.append_to_history() + + # Stop search and focus previous control again. + stop_search(target_buffer_control) + + +def _get_reverse_search_links( + layout: "Layout", +) -> Dict["BufferControl", "SearchBufferControl"]: + """ + Return mapping from BufferControl to SearchBufferControl. + """ + return { + buffer_control: search_buffer_control + for search_buffer_control, buffer_control in layout.search_links.items() + } diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/selection.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/selection.py index 4dc4bf38e0..5d2545bd11 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/selection.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/selection.py @@ -1,60 +1,60 @@ -""" -Data structures for the selection. -""" -from enum import Enum - -__all__ = [ - "SelectionType", - "PasteMode", - "SelectionState", -] - - -class SelectionType(Enum): - """ - Type of selection. - """ - - #: Characters. (Visual in Vi.) - CHARACTERS = "CHARACTERS" - - #: Whole lines. (Visual-Line in Vi.) - LINES = "LINES" - - #: A block selection. (Visual-Block in Vi.) - BLOCK = "BLOCK" - - -class PasteMode(Enum): - EMACS = "EMACS" # Yank like emacs. - VI_AFTER = "VI_AFTER" # When pressing 'p' in Vi. - VI_BEFORE = "VI_BEFORE" # When pressing 'P' in Vi. - - -class SelectionState: - """ - State of the current selection. - - :param original_cursor_position: int - :param type: :class:`~.SelectionType` - """ - - def __init__( - self, - original_cursor_position: int = 0, - type: SelectionType = SelectionType.CHARACTERS, - ) -> None: - - self.original_cursor_position = original_cursor_position - self.type = type - self.shift_mode = False - - def enter_shift_mode(self) -> None: - self.shift_mode = True - - def __repr__(self) -> str: - return "%s(original_cursor_position=%r, type=%r)" % ( - self.__class__.__name__, - self.original_cursor_position, - self.type, - ) +""" +Data structures for the selection. +""" +from enum import Enum + +__all__ = [ + "SelectionType", + "PasteMode", + "SelectionState", +] + + +class SelectionType(Enum): + """ + Type of selection. + """ + + #: Characters. (Visual in Vi.) + CHARACTERS = "CHARACTERS" + + #: Whole lines. (Visual-Line in Vi.) + LINES = "LINES" + + #: A block selection. (Visual-Block in Vi.) + BLOCK = "BLOCK" + + +class PasteMode(Enum): + EMACS = "EMACS" # Yank like emacs. + VI_AFTER = "VI_AFTER" # When pressing 'p' in Vi. + VI_BEFORE = "VI_BEFORE" # When pressing 'P' in Vi. + + +class SelectionState: + """ + State of the current selection. + + :param original_cursor_position: int + :param type: :class:`~.SelectionType` + """ + + def __init__( + self, + original_cursor_position: int = 0, + type: SelectionType = SelectionType.CHARACTERS, + ) -> None: + + self.original_cursor_position = original_cursor_position + self.type = type + self.shift_mode = False + + def enter_shift_mode(self) -> None: + self.shift_mode = True + + def __repr__(self) -> str: + return "%s(original_cursor_position=%r, type=%r)" % ( + self.__class__.__name__, + self.original_cursor_position, + self.type, + ) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/__init__.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/__init__.py index 91436e973b..10ad31483e 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/__init__.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/__init__.py @@ -1,44 +1,44 @@ -from .dialogs import ( - button_dialog, - checkboxlist_dialog, - input_dialog, - message_dialog, - progress_dialog, - radiolist_dialog, - yes_no_dialog, -) -from .progress_bar import ProgressBar, ProgressBarCounter -from .prompt import ( - CompleteStyle, - PromptSession, - confirm, - create_confirm_session, - prompt, -) -from .utils import clear, clear_title, print_container, print_formatted_text, set_title - -__all__ = [ - # Dialogs. - "input_dialog", - "message_dialog", - "progress_dialog", - "checkboxlist_dialog", - "radiolist_dialog", - "yes_no_dialog", - "button_dialog", - # Prompts. - "PromptSession", - "prompt", - "confirm", - "create_confirm_session", - "CompleteStyle", - # Progress bars. - "ProgressBar", - "ProgressBarCounter", - # Utils. - "clear", - "clear_title", - "print_container", - "print_formatted_text", - "set_title", -] +from .dialogs import ( + button_dialog, + checkboxlist_dialog, + input_dialog, + message_dialog, + progress_dialog, + radiolist_dialog, + yes_no_dialog, +) +from .progress_bar import ProgressBar, ProgressBarCounter +from .prompt import ( + CompleteStyle, + PromptSession, + confirm, + create_confirm_session, + prompt, +) +from .utils import clear, clear_title, print_container, print_formatted_text, set_title + +__all__ = [ + # Dialogs. + "input_dialog", + "message_dialog", + "progress_dialog", + "checkboxlist_dialog", + "radiolist_dialog", + "yes_no_dialog", + "button_dialog", + # Prompts. + "PromptSession", + "prompt", + "confirm", + "create_confirm_session", + "CompleteStyle", + # Progress bars. + "ProgressBar", + "ProgressBarCounter", + # Utils. + "clear", + "clear_title", + "print_container", + "print_formatted_text", + "set_title", +] diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/dialogs.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/dialogs.py index 453b49cba2..a91bb3142c 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/dialogs.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/dialogs.py @@ -1,323 +1,323 @@ -import functools -from typing import Any, Callable, List, Optional, Tuple, TypeVar - -from prompt_toolkit.application import Application -from prompt_toolkit.application.current import get_app -from prompt_toolkit.buffer import Buffer -from prompt_toolkit.completion import Completer -from prompt_toolkit.eventloop import get_event_loop, run_in_executor_with_context -from prompt_toolkit.filters import FilterOrBool -from prompt_toolkit.formatted_text import AnyFormattedText -from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous -from prompt_toolkit.key_binding.defaults import load_key_bindings -from prompt_toolkit.key_binding.key_bindings import KeyBindings, merge_key_bindings -from prompt_toolkit.layout import Layout -from prompt_toolkit.layout.containers import AnyContainer, HSplit -from prompt_toolkit.layout.dimension import Dimension as D -from prompt_toolkit.styles import BaseStyle -from prompt_toolkit.validation import Validator -from prompt_toolkit.widgets import ( - Box, - Button, - CheckboxList, - Dialog, - Label, - ProgressBar, - RadioList, - TextArea, - ValidationToolbar, -) - -__all__ = [ - "yes_no_dialog", - "button_dialog", - "input_dialog", - "message_dialog", - "radiolist_dialog", - "checkboxlist_dialog", - "progress_dialog", -] - - -def yes_no_dialog( - title: AnyFormattedText = "", - text: AnyFormattedText = "", - yes_text: str = "Yes", - no_text: str = "No", - style: Optional[BaseStyle] = None, -) -> Application[bool]: - """ - Display a Yes/No dialog. - Return a boolean. - """ - - def yes_handler() -> None: - get_app().exit(result=True) - - def no_handler() -> None: - get_app().exit(result=False) - - dialog = Dialog( - title=title, - body=Label(text=text, dont_extend_height=True), - buttons=[ - Button(text=yes_text, handler=yes_handler), - Button(text=no_text, handler=no_handler), - ], - with_background=True, - ) - - return _create_app(dialog, style) - - -_T = TypeVar("_T") - - -def button_dialog( - title: AnyFormattedText = "", - text: AnyFormattedText = "", - buttons: List[Tuple[str, _T]] = [], - style: Optional[BaseStyle] = None, -) -> Application[_T]: - """ - Display a dialog with button choices (given as a list of tuples). - Return the value associated with button. - """ - - def button_handler(v: _T) -> None: - get_app().exit(result=v) - - dialog = Dialog( - title=title, - body=Label(text=text, dont_extend_height=True), - buttons=[ - Button(text=t, handler=functools.partial(button_handler, v)) - for t, v in buttons - ], - with_background=True, - ) - - return _create_app(dialog, style) - - -def input_dialog( - title: AnyFormattedText = "", - text: AnyFormattedText = "", - ok_text: str = "OK", - cancel_text: str = "Cancel", - completer: Optional[Completer] = None, - validator: Optional[Validator] = None, - password: FilterOrBool = False, - style: Optional[BaseStyle] = None, -) -> Application[str]: - """ - Display a text input box. - Return the given text, or None when cancelled. - """ - - def accept(buf: Buffer) -> bool: - get_app().layout.focus(ok_button) - return True # Keep text. - - def ok_handler() -> None: - get_app().exit(result=textfield.text) - - ok_button = Button(text=ok_text, handler=ok_handler) - cancel_button = Button(text=cancel_text, handler=_return_none) - - textfield = TextArea( - multiline=False, - password=password, - completer=completer, - validator=validator, - accept_handler=accept, - ) - - dialog = Dialog( - title=title, - body=HSplit( - [ - Label(text=text, dont_extend_height=True), - textfield, - ValidationToolbar(), - ], - padding=D(preferred=1, max=1), - ), - buttons=[ok_button, cancel_button], - with_background=True, - ) - - return _create_app(dialog, style) - - -def message_dialog( - title: AnyFormattedText = "", - text: AnyFormattedText = "", - ok_text: str = "Ok", - style: Optional[BaseStyle] = None, -) -> Application[None]: - """ - Display a simple message box and wait until the user presses enter. - """ - dialog = Dialog( - title=title, - body=Label(text=text, dont_extend_height=True), - buttons=[Button(text=ok_text, handler=_return_none)], - with_background=True, - ) - - return _create_app(dialog, style) - - -def radiolist_dialog( - title: AnyFormattedText = "", - text: AnyFormattedText = "", - ok_text: str = "Ok", - cancel_text: str = "Cancel", - values: Optional[List[Tuple[_T, AnyFormattedText]]] = None, - style: Optional[BaseStyle] = None, -) -> Application[_T]: - """ - Display a simple list of element the user can choose amongst. - - Only one element can be selected at a time using Arrow keys and Enter. - The focus can be moved between the list and the Ok/Cancel button with tab. - """ - if values is None: - values = [] - - def ok_handler() -> None: - get_app().exit(result=radio_list.current_value) - - radio_list = RadioList(values) - - dialog = Dialog( - title=title, - body=HSplit( - [Label(text=text, dont_extend_height=True), radio_list], - padding=1, - ), - buttons=[ - Button(text=ok_text, handler=ok_handler), - Button(text=cancel_text, handler=_return_none), - ], - with_background=True, - ) - - return _create_app(dialog, style) - - -def checkboxlist_dialog( - title: AnyFormattedText = "", - text: AnyFormattedText = "", - ok_text: str = "Ok", - cancel_text: str = "Cancel", - values: Optional[List[Tuple[_T, AnyFormattedText]]] = None, - style: Optional[BaseStyle] = None, -) -> Application[List[_T]]: - """ - Display a simple list of element the user can choose multiple values amongst. - - Several elements can be selected at a time using Arrow keys and Enter. - The focus can be moved between the list and the Ok/Cancel button with tab. - """ - if values is None: - values = [] - - def ok_handler() -> None: - get_app().exit(result=cb_list.current_values) - - cb_list = CheckboxList(values) - - dialog = Dialog( - title=title, - body=HSplit( - [Label(text=text, dont_extend_height=True), cb_list], - padding=1, - ), - buttons=[ - Button(text=ok_text, handler=ok_handler), - Button(text=cancel_text, handler=_return_none), - ], - with_background=True, - ) - - return _create_app(dialog, style) - - -def progress_dialog( - title: AnyFormattedText = "", - text: AnyFormattedText = "", - run_callback: Callable[[Callable[[int], None], Callable[[str], None]], None] = ( - lambda *a: None - ), - style: Optional[BaseStyle] = None, -) -> Application[None]: - """ - :param run_callback: A function that receives as input a `set_percentage` - function and it does the work. - """ - loop = get_event_loop() - progressbar = ProgressBar() - text_area = TextArea( - focusable=False, - # Prefer this text area as big as possible, to avoid having a window - # that keeps resizing when we add text to it. +import functools +from typing import Any, Callable, List, Optional, Tuple, TypeVar + +from prompt_toolkit.application import Application +from prompt_toolkit.application.current import get_app +from prompt_toolkit.buffer import Buffer +from prompt_toolkit.completion import Completer +from prompt_toolkit.eventloop import get_event_loop, run_in_executor_with_context +from prompt_toolkit.filters import FilterOrBool +from prompt_toolkit.formatted_text import AnyFormattedText +from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous +from prompt_toolkit.key_binding.defaults import load_key_bindings +from prompt_toolkit.key_binding.key_bindings import KeyBindings, merge_key_bindings +from prompt_toolkit.layout import Layout +from prompt_toolkit.layout.containers import AnyContainer, HSplit +from prompt_toolkit.layout.dimension import Dimension as D +from prompt_toolkit.styles import BaseStyle +from prompt_toolkit.validation import Validator +from prompt_toolkit.widgets import ( + Box, + Button, + CheckboxList, + Dialog, + Label, + ProgressBar, + RadioList, + TextArea, + ValidationToolbar, +) + +__all__ = [ + "yes_no_dialog", + "button_dialog", + "input_dialog", + "message_dialog", + "radiolist_dialog", + "checkboxlist_dialog", + "progress_dialog", +] + + +def yes_no_dialog( + title: AnyFormattedText = "", + text: AnyFormattedText = "", + yes_text: str = "Yes", + no_text: str = "No", + style: Optional[BaseStyle] = None, +) -> Application[bool]: + """ + Display a Yes/No dialog. + Return a boolean. + """ + + def yes_handler() -> None: + get_app().exit(result=True) + + def no_handler() -> None: + get_app().exit(result=False) + + dialog = Dialog( + title=title, + body=Label(text=text, dont_extend_height=True), + buttons=[ + Button(text=yes_text, handler=yes_handler), + Button(text=no_text, handler=no_handler), + ], + with_background=True, + ) + + return _create_app(dialog, style) + + +_T = TypeVar("_T") + + +def button_dialog( + title: AnyFormattedText = "", + text: AnyFormattedText = "", + buttons: List[Tuple[str, _T]] = [], + style: Optional[BaseStyle] = None, +) -> Application[_T]: + """ + Display a dialog with button choices (given as a list of tuples). + Return the value associated with button. + """ + + def button_handler(v: _T) -> None: + get_app().exit(result=v) + + dialog = Dialog( + title=title, + body=Label(text=text, dont_extend_height=True), + buttons=[ + Button(text=t, handler=functools.partial(button_handler, v)) + for t, v in buttons + ], + with_background=True, + ) + + return _create_app(dialog, style) + + +def input_dialog( + title: AnyFormattedText = "", + text: AnyFormattedText = "", + ok_text: str = "OK", + cancel_text: str = "Cancel", + completer: Optional[Completer] = None, + validator: Optional[Validator] = None, + password: FilterOrBool = False, + style: Optional[BaseStyle] = None, +) -> Application[str]: + """ + Display a text input box. + Return the given text, or None when cancelled. + """ + + def accept(buf: Buffer) -> bool: + get_app().layout.focus(ok_button) + return True # Keep text. + + def ok_handler() -> None: + get_app().exit(result=textfield.text) + + ok_button = Button(text=ok_text, handler=ok_handler) + cancel_button = Button(text=cancel_text, handler=_return_none) + + textfield = TextArea( + multiline=False, + password=password, + completer=completer, + validator=validator, + accept_handler=accept, + ) + + dialog = Dialog( + title=title, + body=HSplit( + [ + Label(text=text, dont_extend_height=True), + textfield, + ValidationToolbar(), + ], + padding=D(preferred=1, max=1), + ), + buttons=[ok_button, cancel_button], + with_background=True, + ) + + return _create_app(dialog, style) + + +def message_dialog( + title: AnyFormattedText = "", + text: AnyFormattedText = "", + ok_text: str = "Ok", + style: Optional[BaseStyle] = None, +) -> Application[None]: + """ + Display a simple message box and wait until the user presses enter. + """ + dialog = Dialog( + title=title, + body=Label(text=text, dont_extend_height=True), + buttons=[Button(text=ok_text, handler=_return_none)], + with_background=True, + ) + + return _create_app(dialog, style) + + +def radiolist_dialog( + title: AnyFormattedText = "", + text: AnyFormattedText = "", + ok_text: str = "Ok", + cancel_text: str = "Cancel", + values: Optional[List[Tuple[_T, AnyFormattedText]]] = None, + style: Optional[BaseStyle] = None, +) -> Application[_T]: + """ + Display a simple list of element the user can choose amongst. + + Only one element can be selected at a time using Arrow keys and Enter. + The focus can be moved between the list and the Ok/Cancel button with tab. + """ + if values is None: + values = [] + + def ok_handler() -> None: + get_app().exit(result=radio_list.current_value) + + radio_list = RadioList(values) + + dialog = Dialog( + title=title, + body=HSplit( + [Label(text=text, dont_extend_height=True), radio_list], + padding=1, + ), + buttons=[ + Button(text=ok_text, handler=ok_handler), + Button(text=cancel_text, handler=_return_none), + ], + with_background=True, + ) + + return _create_app(dialog, style) + + +def checkboxlist_dialog( + title: AnyFormattedText = "", + text: AnyFormattedText = "", + ok_text: str = "Ok", + cancel_text: str = "Cancel", + values: Optional[List[Tuple[_T, AnyFormattedText]]] = None, + style: Optional[BaseStyle] = None, +) -> Application[List[_T]]: + """ + Display a simple list of element the user can choose multiple values amongst. + + Several elements can be selected at a time using Arrow keys and Enter. + The focus can be moved between the list and the Ok/Cancel button with tab. + """ + if values is None: + values = [] + + def ok_handler() -> None: + get_app().exit(result=cb_list.current_values) + + cb_list = CheckboxList(values) + + dialog = Dialog( + title=title, + body=HSplit( + [Label(text=text, dont_extend_height=True), cb_list], + padding=1, + ), + buttons=[ + Button(text=ok_text, handler=ok_handler), + Button(text=cancel_text, handler=_return_none), + ], + with_background=True, + ) + + return _create_app(dialog, style) + + +def progress_dialog( + title: AnyFormattedText = "", + text: AnyFormattedText = "", + run_callback: Callable[[Callable[[int], None], Callable[[str], None]], None] = ( + lambda *a: None + ), + style: Optional[BaseStyle] = None, +) -> Application[None]: + """ + :param run_callback: A function that receives as input a `set_percentage` + function and it does the work. + """ + loop = get_event_loop() + progressbar = ProgressBar() + text_area = TextArea( + focusable=False, + # Prefer this text area as big as possible, to avoid having a window + # that keeps resizing when we add text to it. height=D(preferred=10**10), - ) - - dialog = Dialog( - body=HSplit( - [ - Box(Label(text=text)), - Box(text_area, padding=D.exact(1)), - progressbar, - ] - ), - title=title, - with_background=True, - ) - app = _create_app(dialog, style) - - def set_percentage(value: int) -> None: - progressbar.percentage = int(value) - app.invalidate() - - def log_text(text: str) -> None: - loop.call_soon_threadsafe(text_area.buffer.insert_text, text) - app.invalidate() - - # Run the callback in the executor. When done, set a return value for the - # UI, so that it quits. - def start() -> None: - try: - run_callback(set_percentage, log_text) - finally: - app.exit() - - def pre_run() -> None: - run_in_executor_with_context(start) - - app.pre_run_callables.append(pre_run) - - return app - - -def _create_app(dialog: AnyContainer, style: Optional[BaseStyle]) -> Application[Any]: - # Key bindings. - bindings = KeyBindings() - bindings.add("tab")(focus_next) - bindings.add("s-tab")(focus_previous) - - return Application( - layout=Layout(dialog), - key_bindings=merge_key_bindings([load_key_bindings(), bindings]), - mouse_support=True, - style=style, - full_screen=True, - ) - - -def _return_none() -> None: - "Button handler that returns None." - get_app().exit() + ) + + dialog = Dialog( + body=HSplit( + [ + Box(Label(text=text)), + Box(text_area, padding=D.exact(1)), + progressbar, + ] + ), + title=title, + with_background=True, + ) + app = _create_app(dialog, style) + + def set_percentage(value: int) -> None: + progressbar.percentage = int(value) + app.invalidate() + + def log_text(text: str) -> None: + loop.call_soon_threadsafe(text_area.buffer.insert_text, text) + app.invalidate() + + # Run the callback in the executor. When done, set a return value for the + # UI, so that it quits. + def start() -> None: + try: + run_callback(set_percentage, log_text) + finally: + app.exit() + + def pre_run() -> None: + run_in_executor_with_context(start) + + app.pre_run_callables.append(pre_run) + + return app + + +def _create_app(dialog: AnyContainer, style: Optional[BaseStyle]) -> Application[Any]: + # Key bindings. + bindings = KeyBindings() + bindings.add("tab")(focus_next) + bindings.add("s-tab")(focus_previous) + + return Application( + layout=Layout(dialog), + key_bindings=merge_key_bindings([load_key_bindings(), bindings]), + mouse_support=True, + style=style, + full_screen=True, + ) + + +def _return_none() -> None: + "Button handler that returns None." + get_app().exit() diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/progress_bar/__init__.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/progress_bar/__init__.py index 5df9f61f35..7d0fbb50d5 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/progress_bar/__init__.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/progress_bar/__init__.py @@ -1,31 +1,31 @@ -from .base import ProgressBar, ProgressBarCounter -from .formatters import ( - Bar, - Formatter, - IterationsPerSecond, - Label, - Percentage, - Progress, - Rainbow, - SpinningWheel, - Text, - TimeElapsed, - TimeLeft, -) - -__all__ = [ - "ProgressBar", - "ProgressBarCounter", - # Formatters. - "Formatter", - "Text", - "Label", - "Percentage", - "Bar", - "Progress", - "TimeElapsed", - "TimeLeft", - "IterationsPerSecond", - "SpinningWheel", - "Rainbow", -] +from .base import ProgressBar, ProgressBarCounter +from .formatters import ( + Bar, + Formatter, + IterationsPerSecond, + Label, + Percentage, + Progress, + Rainbow, + SpinningWheel, + Text, + TimeElapsed, + TimeLeft, +) + +__all__ = [ + "ProgressBar", + "ProgressBarCounter", + # Formatters. + "Formatter", + "Text", + "Label", + "Percentage", + "Bar", + "Progress", + "TimeElapsed", + "TimeLeft", + "IterationsPerSecond", + "SpinningWheel", + "Rainbow", +] diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/progress_bar/base.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/progress_bar/base.py index df1feb4a93..c22507e25c 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/progress_bar/base.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/progress_bar/base.py @@ -1,438 +1,438 @@ -""" -Progress bar implementation on top of prompt_toolkit. - -:: - - with ProgressBar(...) as pb: - for item in pb(data): - ... -""" -import datetime -import functools -import os -import signal -import threading -import traceback -from asyncio import new_event_loop, set_event_loop -from typing import ( - Generic, - Iterable, - Iterator, - List, - Optional, - Sequence, - Sized, - TextIO, - TypeVar, - cast, -) - -from prompt_toolkit.application import Application -from prompt_toolkit.application.current import get_app_session -from prompt_toolkit.eventloop import get_event_loop -from prompt_toolkit.filters import Condition, is_done, renderer_height_is_known -from prompt_toolkit.formatted_text import ( - AnyFormattedText, - StyleAndTextTuples, - to_formatted_text, -) -from prompt_toolkit.input import Input -from prompt_toolkit.key_binding import KeyBindings -from prompt_toolkit.key_binding.key_processor import KeyPressEvent -from prompt_toolkit.layout import ( - ConditionalContainer, - FormattedTextControl, - HSplit, - Layout, - VSplit, - Window, -) -from prompt_toolkit.layout.controls import UIContent, UIControl -from prompt_toolkit.layout.dimension import AnyDimension, D -from prompt_toolkit.output import ColorDepth, Output -from prompt_toolkit.styles import BaseStyle - -from .formatters import Formatter, create_default_formatters - -try: - import contextvars -except ImportError: - from prompt_toolkit.eventloop import dummy_contextvars - - contextvars = dummy_contextvars # type: ignore - - -__all__ = ["ProgressBar"] - -E = KeyPressEvent - -_SIGWINCH = getattr(signal, "SIGWINCH", None) - - -def create_key_bindings() -> KeyBindings: - """ - Key bindings handled by the progress bar. - (The main thread is not supposed to handle any key bindings.) - """ - kb = KeyBindings() - - @kb.add("c-l") - def _clear(event: E) -> None: - event.app.renderer.clear() - - @kb.add("c-c") - def _interrupt(event: E) -> None: - # Send KeyboardInterrupt to the main thread. - os.kill(os.getpid(), signal.SIGINT) - - return kb - - -_T = TypeVar("_T") - - -class ProgressBar: - """ - Progress bar context manager. - - Usage :: - - with ProgressBar(...) as pb: - for item in pb(data): - ... - - :param title: Text to be displayed above the progress bars. This can be a - callable or formatted text as well. - :param formatters: List of :class:`.Formatter` instances. - :param bottom_toolbar: Text to be displayed in the bottom toolbar. This - can be a callable or formatted text. - :param style: :class:`prompt_toolkit.styles.BaseStyle` instance. - :param key_bindings: :class:`.KeyBindings` instance. - :param file: The file object used for rendering, by default `sys.stderr` is used. - - :param color_depth: `prompt_toolkit` `ColorDepth` instance. - :param output: :class:`~prompt_toolkit.output.Output` instance. - :param input: :class:`~prompt_toolkit.input.Input` instance. - """ - - def __init__( - self, - title: AnyFormattedText = None, - formatters: Optional[Sequence[Formatter]] = None, - bottom_toolbar: AnyFormattedText = None, - style: Optional[BaseStyle] = None, - key_bindings: Optional[KeyBindings] = None, - file: Optional[TextIO] = None, - color_depth: Optional[ColorDepth] = None, - output: Optional[Output] = None, - input: Optional[Input] = None, - ) -> None: - - self.title = title - self.formatters = formatters or create_default_formatters() - self.bottom_toolbar = bottom_toolbar - self.counters: List[ProgressBarCounter[object]] = [] - self.style = style - self.key_bindings = key_bindings - - # Note that we use __stderr__ as default error output, because that - # works best with `patch_stdout`. - self.color_depth = color_depth - self.output = output or get_app_session().output - self.input = input or get_app_session().input - - self._thread: Optional[threading.Thread] = None - - self._loop = get_event_loop() - self._app_loop = new_event_loop() - self._has_sigwinch = False - self._app_started = threading.Event() - - def __enter__(self) -> "ProgressBar": - # Create UI Application. - title_toolbar = ConditionalContainer( - Window( - FormattedTextControl(lambda: self.title), - height=1, - style="class:progressbar,title", - ), - filter=Condition(lambda: self.title is not None), - ) - - bottom_toolbar = ConditionalContainer( - Window( - FormattedTextControl( - lambda: self.bottom_toolbar, style="class:bottom-toolbar.text" - ), - style="class:bottom-toolbar", - height=1, - ), - filter=~is_done - & renderer_height_is_known - & Condition(lambda: self.bottom_toolbar is not None), - ) - - def width_for_formatter(formatter: Formatter) -> AnyDimension: - # Needs to be passed as callable (partial) to the 'width' - # parameter, because we want to call it on every resize. - return formatter.get_width(progress_bar=self) - - progress_controls = [ - Window( - content=_ProgressControl(self, f), - width=functools.partial(width_for_formatter, f), - ) - for f in self.formatters - ] - - self.app: Application[None] = Application( - min_redraw_interval=0.05, - layout=Layout( - HSplit( - [ - title_toolbar, - VSplit( - progress_controls, - height=lambda: D( - preferred=len(self.counters), max=len(self.counters) - ), - ), - Window(), - bottom_toolbar, - ] - ) - ), - style=self.style, - key_bindings=self.key_bindings, - refresh_interval=0.3, - color_depth=self.color_depth, - output=self.output, - input=self.input, - ) - - # Run application in different thread. - def run() -> None: - set_event_loop(self._app_loop) - try: - self.app.run(pre_run=self._app_started.set) - except BaseException as e: - traceback.print_exc() - print(e) - - ctx: contextvars.Context = contextvars.copy_context() - - self._thread = threading.Thread(target=ctx.run, args=(run,)) - self._thread.start() - - return self - - def __exit__(self, *a: object) -> None: - # Wait for the app to be started. Make sure we don't quit earlier, - # otherwise `self.app.exit` won't terminate the app because - # `self.app.future` has not yet been set. - self._app_started.wait() - - # Quit UI application. - if self.app.is_running: - self._app_loop.call_soon_threadsafe(self.app.exit) - - if self._thread is not None: - self._thread.join() - self._app_loop.close() - - def __call__( - self, - data: Optional[Iterable[_T]] = None, - label: AnyFormattedText = "", - remove_when_done: bool = False, - total: Optional[int] = None, - ) -> "ProgressBarCounter[_T]": - """ - Start a new counter. - - :param label: Title text or description for this progress. (This can be - formatted text as well). - :param remove_when_done: When `True`, hide this progress bar. - :param total: Specify the maximum value if it can't be calculated by - calling ``len``. - """ - counter = ProgressBarCounter( - self, data, label=label, remove_when_done=remove_when_done, total=total - ) - self.counters.append(counter) - return counter - - def invalidate(self) -> None: - self.app.invalidate() - - -class _ProgressControl(UIControl): - """ - User control for the progress bar. - """ - - def __init__(self, progress_bar: ProgressBar, formatter: Formatter) -> None: - self.progress_bar = progress_bar - self.formatter = formatter - self._key_bindings = create_key_bindings() - - def create_content(self, width: int, height: int) -> UIContent: - items: List[StyleAndTextTuples] = [] - - for pr in self.progress_bar.counters: - try: - text = self.formatter.format(self.progress_bar, pr, width) - except BaseException: - traceback.print_exc() - text = "ERROR" - - items.append(to_formatted_text(text)) - - def get_line(i: int) -> StyleAndTextTuples: - return items[i] - - return UIContent(get_line=get_line, line_count=len(items), show_cursor=False) - - def is_focusable(self) -> bool: - return True # Make sure that the key bindings work. - - def get_key_bindings(self) -> KeyBindings: - return self._key_bindings - - -_CounterItem = TypeVar("_CounterItem", covariant=True) - - -class ProgressBarCounter(Generic[_CounterItem]): - """ - An individual counter (A progress bar can have multiple counters). - """ - - def __init__( - self, - progress_bar: ProgressBar, - data: Optional[Iterable[_CounterItem]] = None, - label: AnyFormattedText = "", - remove_when_done: bool = False, - total: Optional[int] = None, - ) -> None: - - self.start_time = datetime.datetime.now() - self.stop_time: Optional[datetime.datetime] = None - self.progress_bar = progress_bar - self.data = data - self.items_completed = 0 - self.label = label - self.remove_when_done = remove_when_done - self._done = False - self.total: Optional[int] - - if total is None: - try: - self.total = len(cast(Sized, data)) - except TypeError: - self.total = None # We don't know the total length. - else: - self.total = total - - def __iter__(self) -> Iterator[_CounterItem]: - if self.data is not None: - try: - for item in self.data: - yield item - self.item_completed() - - # Only done if we iterate to the very end. - self.done = True - finally: - # Ensure counter has stopped even if we did not iterate to the - # end (e.g. break or exceptions). - self.stopped = True - else: - raise NotImplementedError("No data defined to iterate over.") - - def item_completed(self) -> None: - """ - Start handling the next item. - - (Can be called manually in case we don't have a collection to loop through.) - """ - self.items_completed += 1 - self.progress_bar.invalidate() - - @property - def done(self) -> bool: - """Whether a counter has been completed. - - Done counter have been stopped (see stopped) and removed depending on - remove_when_done value. - - Contrast this with stopped. A stopped counter may be terminated before - 100% completion. A done counter has reached its 100% completion. - """ - return self._done - - @done.setter - def done(self, value: bool) -> None: - self._done = value - self.stopped = value - - if value and self.remove_when_done: - self.progress_bar.counters.remove(self) - - @property - def stopped(self) -> bool: - """Whether a counter has been stopped. - - Stopped counters no longer have increasing time_elapsed. This distinction is - also used to prevent the Bar formatter with unknown totals from continuing to run. - - A stopped counter (but not done) can be used to signal that a given counter has - encountered an error but allows other counters to continue - (e.g. download X of Y failed). Given how only done counters are removed - (see remove_when_done) this can help aggregate failures from a large number of - successes. - - Contrast this with done. A done counter has reached its 100% completion. - A stopped counter may be terminated before 100% completion. - """ - return self.stop_time is not None - - @stopped.setter - def stopped(self, value: bool) -> None: - if value: - # This counter has not already been stopped. - if not self.stop_time: - self.stop_time = datetime.datetime.now() - else: - # Clearing any previously set stop_time. - self.stop_time = None - - @property - def percentage(self) -> float: - if self.total is None: - return 0 - else: - return self.items_completed * 100 / max(self.total, 1) - - @property - def time_elapsed(self) -> datetime.timedelta: - """ - Return how much time has been elapsed since the start. - """ - if self.stop_time is None: - return datetime.datetime.now() - self.start_time - else: - return self.stop_time - self.start_time - - @property - def time_left(self) -> Optional[datetime.timedelta]: - """ - Timedelta representing the time left. - """ - if self.total is None or not self.percentage: - return None - elif self.done or self.stopped: - return datetime.timedelta(0) - else: - return self.time_elapsed * (100 - self.percentage) / self.percentage +""" +Progress bar implementation on top of prompt_toolkit. + +:: + + with ProgressBar(...) as pb: + for item in pb(data): + ... +""" +import datetime +import functools +import os +import signal +import threading +import traceback +from asyncio import new_event_loop, set_event_loop +from typing import ( + Generic, + Iterable, + Iterator, + List, + Optional, + Sequence, + Sized, + TextIO, + TypeVar, + cast, +) + +from prompt_toolkit.application import Application +from prompt_toolkit.application.current import get_app_session +from prompt_toolkit.eventloop import get_event_loop +from prompt_toolkit.filters import Condition, is_done, renderer_height_is_known +from prompt_toolkit.formatted_text import ( + AnyFormattedText, + StyleAndTextTuples, + to_formatted_text, +) +from prompt_toolkit.input import Input +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.key_binding.key_processor import KeyPressEvent +from prompt_toolkit.layout import ( + ConditionalContainer, + FormattedTextControl, + HSplit, + Layout, + VSplit, + Window, +) +from prompt_toolkit.layout.controls import UIContent, UIControl +from prompt_toolkit.layout.dimension import AnyDimension, D +from prompt_toolkit.output import ColorDepth, Output +from prompt_toolkit.styles import BaseStyle + +from .formatters import Formatter, create_default_formatters + +try: + import contextvars +except ImportError: + from prompt_toolkit.eventloop import dummy_contextvars + + contextvars = dummy_contextvars # type: ignore + + +__all__ = ["ProgressBar"] + +E = KeyPressEvent + +_SIGWINCH = getattr(signal, "SIGWINCH", None) + + +def create_key_bindings() -> KeyBindings: + """ + Key bindings handled by the progress bar. + (The main thread is not supposed to handle any key bindings.) + """ + kb = KeyBindings() + + @kb.add("c-l") + def _clear(event: E) -> None: + event.app.renderer.clear() + + @kb.add("c-c") + def _interrupt(event: E) -> None: + # Send KeyboardInterrupt to the main thread. + os.kill(os.getpid(), signal.SIGINT) + + return kb + + +_T = TypeVar("_T") + + +class ProgressBar: + """ + Progress bar context manager. + + Usage :: + + with ProgressBar(...) as pb: + for item in pb(data): + ... + + :param title: Text to be displayed above the progress bars. This can be a + callable or formatted text as well. + :param formatters: List of :class:`.Formatter` instances. + :param bottom_toolbar: Text to be displayed in the bottom toolbar. This + can be a callable or formatted text. + :param style: :class:`prompt_toolkit.styles.BaseStyle` instance. + :param key_bindings: :class:`.KeyBindings` instance. + :param file: The file object used for rendering, by default `sys.stderr` is used. + + :param color_depth: `prompt_toolkit` `ColorDepth` instance. + :param output: :class:`~prompt_toolkit.output.Output` instance. + :param input: :class:`~prompt_toolkit.input.Input` instance. + """ + + def __init__( + self, + title: AnyFormattedText = None, + formatters: Optional[Sequence[Formatter]] = None, + bottom_toolbar: AnyFormattedText = None, + style: Optional[BaseStyle] = None, + key_bindings: Optional[KeyBindings] = None, + file: Optional[TextIO] = None, + color_depth: Optional[ColorDepth] = None, + output: Optional[Output] = None, + input: Optional[Input] = None, + ) -> None: + + self.title = title + self.formatters = formatters or create_default_formatters() + self.bottom_toolbar = bottom_toolbar + self.counters: List[ProgressBarCounter[object]] = [] + self.style = style + self.key_bindings = key_bindings + + # Note that we use __stderr__ as default error output, because that + # works best with `patch_stdout`. + self.color_depth = color_depth + self.output = output or get_app_session().output + self.input = input or get_app_session().input + + self._thread: Optional[threading.Thread] = None + + self._loop = get_event_loop() + self._app_loop = new_event_loop() + self._has_sigwinch = False + self._app_started = threading.Event() + + def __enter__(self) -> "ProgressBar": + # Create UI Application. + title_toolbar = ConditionalContainer( + Window( + FormattedTextControl(lambda: self.title), + height=1, + style="class:progressbar,title", + ), + filter=Condition(lambda: self.title is not None), + ) + + bottom_toolbar = ConditionalContainer( + Window( + FormattedTextControl( + lambda: self.bottom_toolbar, style="class:bottom-toolbar.text" + ), + style="class:bottom-toolbar", + height=1, + ), + filter=~is_done + & renderer_height_is_known + & Condition(lambda: self.bottom_toolbar is not None), + ) + + def width_for_formatter(formatter: Formatter) -> AnyDimension: + # Needs to be passed as callable (partial) to the 'width' + # parameter, because we want to call it on every resize. + return formatter.get_width(progress_bar=self) + + progress_controls = [ + Window( + content=_ProgressControl(self, f), + width=functools.partial(width_for_formatter, f), + ) + for f in self.formatters + ] + + self.app: Application[None] = Application( + min_redraw_interval=0.05, + layout=Layout( + HSplit( + [ + title_toolbar, + VSplit( + progress_controls, + height=lambda: D( + preferred=len(self.counters), max=len(self.counters) + ), + ), + Window(), + bottom_toolbar, + ] + ) + ), + style=self.style, + key_bindings=self.key_bindings, + refresh_interval=0.3, + color_depth=self.color_depth, + output=self.output, + input=self.input, + ) + + # Run application in different thread. + def run() -> None: + set_event_loop(self._app_loop) + try: + self.app.run(pre_run=self._app_started.set) + except BaseException as e: + traceback.print_exc() + print(e) + + ctx: contextvars.Context = contextvars.copy_context() + + self._thread = threading.Thread(target=ctx.run, args=(run,)) + self._thread.start() + + return self + + def __exit__(self, *a: object) -> None: + # Wait for the app to be started. Make sure we don't quit earlier, + # otherwise `self.app.exit` won't terminate the app because + # `self.app.future` has not yet been set. + self._app_started.wait() + + # Quit UI application. + if self.app.is_running: + self._app_loop.call_soon_threadsafe(self.app.exit) + + if self._thread is not None: + self._thread.join() + self._app_loop.close() + + def __call__( + self, + data: Optional[Iterable[_T]] = None, + label: AnyFormattedText = "", + remove_when_done: bool = False, + total: Optional[int] = None, + ) -> "ProgressBarCounter[_T]": + """ + Start a new counter. + + :param label: Title text or description for this progress. (This can be + formatted text as well). + :param remove_when_done: When `True`, hide this progress bar. + :param total: Specify the maximum value if it can't be calculated by + calling ``len``. + """ + counter = ProgressBarCounter( + self, data, label=label, remove_when_done=remove_when_done, total=total + ) + self.counters.append(counter) + return counter + + def invalidate(self) -> None: + self.app.invalidate() + + +class _ProgressControl(UIControl): + """ + User control for the progress bar. + """ + + def __init__(self, progress_bar: ProgressBar, formatter: Formatter) -> None: + self.progress_bar = progress_bar + self.formatter = formatter + self._key_bindings = create_key_bindings() + + def create_content(self, width: int, height: int) -> UIContent: + items: List[StyleAndTextTuples] = [] + + for pr in self.progress_bar.counters: + try: + text = self.formatter.format(self.progress_bar, pr, width) + except BaseException: + traceback.print_exc() + text = "ERROR" + + items.append(to_formatted_text(text)) + + def get_line(i: int) -> StyleAndTextTuples: + return items[i] + + return UIContent(get_line=get_line, line_count=len(items), show_cursor=False) + + def is_focusable(self) -> bool: + return True # Make sure that the key bindings work. + + def get_key_bindings(self) -> KeyBindings: + return self._key_bindings + + +_CounterItem = TypeVar("_CounterItem", covariant=True) + + +class ProgressBarCounter(Generic[_CounterItem]): + """ + An individual counter (A progress bar can have multiple counters). + """ + + def __init__( + self, + progress_bar: ProgressBar, + data: Optional[Iterable[_CounterItem]] = None, + label: AnyFormattedText = "", + remove_when_done: bool = False, + total: Optional[int] = None, + ) -> None: + + self.start_time = datetime.datetime.now() + self.stop_time: Optional[datetime.datetime] = None + self.progress_bar = progress_bar + self.data = data + self.items_completed = 0 + self.label = label + self.remove_when_done = remove_when_done + self._done = False + self.total: Optional[int] + + if total is None: + try: + self.total = len(cast(Sized, data)) + except TypeError: + self.total = None # We don't know the total length. + else: + self.total = total + + def __iter__(self) -> Iterator[_CounterItem]: + if self.data is not None: + try: + for item in self.data: + yield item + self.item_completed() + + # Only done if we iterate to the very end. + self.done = True + finally: + # Ensure counter has stopped even if we did not iterate to the + # end (e.g. break or exceptions). + self.stopped = True + else: + raise NotImplementedError("No data defined to iterate over.") + + def item_completed(self) -> None: + """ + Start handling the next item. + + (Can be called manually in case we don't have a collection to loop through.) + """ + self.items_completed += 1 + self.progress_bar.invalidate() + + @property + def done(self) -> bool: + """Whether a counter has been completed. + + Done counter have been stopped (see stopped) and removed depending on + remove_when_done value. + + Contrast this with stopped. A stopped counter may be terminated before + 100% completion. A done counter has reached its 100% completion. + """ + return self._done + + @done.setter + def done(self, value: bool) -> None: + self._done = value + self.stopped = value + + if value and self.remove_when_done: + self.progress_bar.counters.remove(self) + + @property + def stopped(self) -> bool: + """Whether a counter has been stopped. + + Stopped counters no longer have increasing time_elapsed. This distinction is + also used to prevent the Bar formatter with unknown totals from continuing to run. + + A stopped counter (but not done) can be used to signal that a given counter has + encountered an error but allows other counters to continue + (e.g. download X of Y failed). Given how only done counters are removed + (see remove_when_done) this can help aggregate failures from a large number of + successes. + + Contrast this with done. A done counter has reached its 100% completion. + A stopped counter may be terminated before 100% completion. + """ + return self.stop_time is not None + + @stopped.setter + def stopped(self, value: bool) -> None: + if value: + # This counter has not already been stopped. + if not self.stop_time: + self.stop_time = datetime.datetime.now() + else: + # Clearing any previously set stop_time. + self.stop_time = None + + @property + def percentage(self) -> float: + if self.total is None: + return 0 + else: + return self.items_completed * 100 / max(self.total, 1) + + @property + def time_elapsed(self) -> datetime.timedelta: + """ + Return how much time has been elapsed since the start. + """ + if self.stop_time is None: + return datetime.datetime.now() - self.start_time + else: + return self.stop_time - self.start_time + + @property + def time_left(self) -> Optional[datetime.timedelta]: + """ + Timedelta representing the time left. + """ + if self.total is None or not self.percentage: + return None + elif self.done or self.stopped: + return datetime.timedelta(0) + else: + return self.time_elapsed * (100 - self.percentage) / self.percentage diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/progress_bar/formatters.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/progress_bar/formatters.py index 855bbed336..ad3a932d3f 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/progress_bar/formatters.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/progress_bar/formatters.py @@ -1,436 +1,436 @@ -""" -Formatter classes for the progress bar. -Each progress bar consists of a list of these formatters. -""" -import datetime -import time -from abc import ABCMeta, abstractmethod -from typing import TYPE_CHECKING, List, Tuple - -from prompt_toolkit.formatted_text import ( - HTML, - AnyFormattedText, - StyleAndTextTuples, - to_formatted_text, -) -from prompt_toolkit.formatted_text.utils import fragment_list_width -from prompt_toolkit.layout.dimension import AnyDimension, D -from prompt_toolkit.layout.utils import explode_text_fragments -from prompt_toolkit.utils import get_cwidth - -if TYPE_CHECKING: - from .base import ProgressBar, ProgressBarCounter - -__all__ = [ - "Formatter", - "Text", - "Label", - "Percentage", - "Bar", - "Progress", - "TimeElapsed", - "TimeLeft", - "IterationsPerSecond", - "SpinningWheel", - "Rainbow", - "create_default_formatters", -] - - -class Formatter(metaclass=ABCMeta): - """ - Base class for any formatter. - """ - - @abstractmethod - def format( - self, - progress_bar: "ProgressBar", - progress: "ProgressBarCounter[object]", - width: int, - ) -> AnyFormattedText: - pass - - def get_width(self, progress_bar: "ProgressBar") -> AnyDimension: - return D() - - -class Text(Formatter): - """ - Display plain text. - """ - - def __init__(self, text: AnyFormattedText, style: str = "") -> None: - self.text = to_formatted_text(text, style=style) - - def format( - self, - progress_bar: "ProgressBar", - progress: "ProgressBarCounter[object]", - width: int, - ) -> AnyFormattedText: - return self.text - - def get_width(self, progress_bar: "ProgressBar") -> AnyDimension: - return fragment_list_width(self.text) - - -class Label(Formatter): - """ - Display the name of the current task. - - :param width: If a `width` is given, use this width. Scroll the text if it - doesn't fit in this width. - :param suffix: String suffix to be added after the task name, e.g. ': '. - If no task name was given, no suffix will be added. - """ - - def __init__(self, width: AnyDimension = None, suffix: str = "") -> None: - self.width = width - self.suffix = suffix - - def _add_suffix(self, label: AnyFormattedText) -> StyleAndTextTuples: - label = to_formatted_text(label, style="class:label") - return label + [("", self.suffix)] - - def format( - self, - progress_bar: "ProgressBar", - progress: "ProgressBarCounter[object]", - width: int, - ) -> AnyFormattedText: - - label = self._add_suffix(progress.label) - cwidth = fragment_list_width(label) - - if cwidth > width: - # It doesn't fit -> scroll task name. - label = explode_text_fragments(label) - max_scroll = cwidth - width - current_scroll = int(time.time() * 3 % max_scroll) - label = label[current_scroll:] - - return label - - def get_width(self, progress_bar: "ProgressBar") -> AnyDimension: - if self.width: - return self.width - - all_labels = [self._add_suffix(c.label) for c in progress_bar.counters] - if all_labels: - max_widths = max(fragment_list_width(l) for l in all_labels) - return D(preferred=max_widths, max=max_widths) - else: - return D() - - -class Percentage(Formatter): - """ - Display the progress as a percentage. - """ - - template = "<percentage>{percentage:>5}%</percentage>" - - def format( - self, - progress_bar: "ProgressBar", - progress: "ProgressBarCounter[object]", - width: int, - ) -> AnyFormattedText: - - return HTML(self.template).format(percentage=round(progress.percentage, 1)) - - def get_width(self, progress_bar: "ProgressBar") -> AnyDimension: - return D.exact(6) - - -class Bar(Formatter): - """ - Display the progress bar itself. - """ - - template = "<bar>{start}<bar-a>{bar_a}</bar-a><bar-b>{bar_b}</bar-b><bar-c>{bar_c}</bar-c>{end}</bar>" - - def __init__( - self, - start: str = "[", - end: str = "]", - sym_a: str = "=", - sym_b: str = ">", - sym_c: str = " ", - unknown: str = "#", - ) -> None: - - assert len(sym_a) == 1 and get_cwidth(sym_a) == 1 - assert len(sym_c) == 1 and get_cwidth(sym_c) == 1 - - self.start = start - self.end = end - self.sym_a = sym_a - self.sym_b = sym_b - self.sym_c = sym_c - self.unknown = unknown - - def format( - self, - progress_bar: "ProgressBar", - progress: "ProgressBarCounter[object]", - width: int, - ) -> AnyFormattedText: - if progress.done or progress.total or progress.stopped: - sym_a, sym_b, sym_c = self.sym_a, self.sym_b, self.sym_c - - # Compute pb_a based on done, total, or stopped states. - if progress.done: - # 100% completed irrelevant of how much was actually marked as completed. - percent = 1.0 - else: - # Show percentage completed. - percent = progress.percentage / 100 - else: - # Total is unknown and bar is still running. - sym_a, sym_b, sym_c = self.sym_c, self.unknown, self.sym_c - - # Compute percent based on the time. - percent = time.time() * 20 % 100 / 100 - - # Subtract left, sym_b, and right. - width -= get_cwidth(self.start + sym_b + self.end) - - # Scale percent by width - pb_a = int(percent * width) - bar_a = sym_a * pb_a - bar_b = sym_b - bar_c = sym_c * (width - pb_a) - - return HTML(self.template).format( - start=self.start, end=self.end, bar_a=bar_a, bar_b=bar_b, bar_c=bar_c - ) - - def get_width(self, progress_bar: "ProgressBar") -> AnyDimension: - return D(min=9) - - -class Progress(Formatter): - """ - Display the progress as text. E.g. "8/20" - """ - - template = "<current>{current:>3}</current>/<total>{total:>3}</total>" - - def format( - self, - progress_bar: "ProgressBar", - progress: "ProgressBarCounter[object]", - width: int, - ) -> AnyFormattedText: - - return HTML(self.template).format( - current=progress.items_completed, total=progress.total or "?" - ) - - def get_width(self, progress_bar: "ProgressBar") -> AnyDimension: - all_lengths = [ - len("{0:>3}".format(c.total or "?")) for c in progress_bar.counters - ] - all_lengths.append(1) - return D.exact(max(all_lengths) * 2 + 1) - - -def _format_timedelta(timedelta: datetime.timedelta) -> str: - """ - Return hh:mm:ss, or mm:ss if the amount of hours is zero. - """ - result = "{0}".format(timedelta).split(".")[0] - if result.startswith("0:"): - result = result[2:] - return result - - -class TimeElapsed(Formatter): - """ - Display the elapsed time. - """ - - def format( - self, - progress_bar: "ProgressBar", - progress: "ProgressBarCounter[object]", - width: int, - ) -> AnyFormattedText: - - text = _format_timedelta(progress.time_elapsed).rjust(width) - return HTML("<time-elapsed>{time_elapsed}</time-elapsed>").format( - time_elapsed=text - ) - - def get_width(self, progress_bar: "ProgressBar") -> AnyDimension: - all_values = [ - len(_format_timedelta(c.time_elapsed)) for c in progress_bar.counters - ] - if all_values: - return max(all_values) - return 0 - - -class TimeLeft(Formatter): - """ - Display the time left. - """ - - template = "<time-left>{time_left}</time-left>" - unknown = "?:??:??" - - def format( - self, - progress_bar: "ProgressBar", - progress: "ProgressBarCounter[object]", - width: int, - ) -> AnyFormattedText: - - time_left = progress.time_left - if time_left is not None: - formatted_time_left = _format_timedelta(time_left) - else: - formatted_time_left = self.unknown - - return HTML(self.template).format(time_left=formatted_time_left.rjust(width)) - - def get_width(self, progress_bar: "ProgressBar") -> AnyDimension: - all_values = [ - len(_format_timedelta(c.time_left)) if c.time_left is not None else 7 - for c in progress_bar.counters - ] - if all_values: - return max(all_values) - return 0 - - -class IterationsPerSecond(Formatter): - """ - Display the iterations per second. - """ - - template = ( - "<iterations-per-second>{iterations_per_second:.2f}</iterations-per-second>" - ) - - def format( - self, - progress_bar: "ProgressBar", - progress: "ProgressBarCounter[object]", - width: int, - ) -> AnyFormattedText: - - value = progress.items_completed / progress.time_elapsed.total_seconds() - return HTML(self.template.format(iterations_per_second=value)) - - def get_width(self, progress_bar: "ProgressBar") -> AnyDimension: - all_values = [ - len("{0:.2f}".format(c.items_completed / c.time_elapsed.total_seconds())) - for c in progress_bar.counters - ] - if all_values: - return max(all_values) - return 0 - - -class SpinningWheel(Formatter): - """ - Display a spinning wheel. - """ - - characters = r"/-\|" - - def format( - self, - progress_bar: "ProgressBar", - progress: "ProgressBarCounter[object]", - width: int, - ) -> AnyFormattedText: - - index = int(time.time() * 3) % len(self.characters) - return HTML("<spinning-wheel>{0}</spinning-wheel>").format( - self.characters[index] - ) - - def get_width(self, progress_bar: "ProgressBar") -> AnyDimension: - return D.exact(1) - - -def _hue_to_rgb(hue: float) -> Tuple[int, int, int]: - """ - Take hue between 0 and 1, return (r, g, b). - """ - i = int(hue * 6.0) - f = (hue * 6.0) - i - - q = int(255 * (1.0 - f)) - t = int(255 * (1.0 - (1.0 - f))) - - i %= 6 - - return [ - (255, t, 0), - (q, 255, 0), - (0, 255, t), - (0, q, 255), - (t, 0, 255), - (255, 0, q), - ][i] - - -class Rainbow(Formatter): - """ - For the fun. Add rainbow colors to any of the other formatters. - """ - - colors = ["#%.2x%.2x%.2x" % _hue_to_rgb(h / 100.0) for h in range(0, 100)] - - def __init__(self, formatter: Formatter) -> None: - self.formatter = formatter - - def format( - self, - progress_bar: "ProgressBar", - progress: "ProgressBarCounter[object]", - width: int, - ) -> AnyFormattedText: - - # Get formatted text from nested formatter, and explode it in - # text/style tuples. - result = self.formatter.format(progress_bar, progress, width) - result = explode_text_fragments(to_formatted_text(result)) - - # Insert colors. - result2: StyleAndTextTuples = [] - shift = int(time.time() * 3) % len(self.colors) - - for i, (style, text, *_) in enumerate(result): - result2.append( - (style + " " + self.colors[(i + shift) % len(self.colors)], text) - ) - return result2 - - def get_width(self, progress_bar: "ProgressBar") -> AnyDimension: - return self.formatter.get_width(progress_bar) - - -def create_default_formatters() -> List[Formatter]: - """ - Return the list of default formatters. - """ - return [ - Label(), - Text(" "), - Percentage(), - Text(" "), - Bar(), - Text(" "), - Progress(), - Text(" "), - Text("eta [", style="class:time-left"), - TimeLeft(), - Text("]", style="class:time-left"), - Text(" "), - ] +""" +Formatter classes for the progress bar. +Each progress bar consists of a list of these formatters. +""" +import datetime +import time +from abc import ABCMeta, abstractmethod +from typing import TYPE_CHECKING, List, Tuple + +from prompt_toolkit.formatted_text import ( + HTML, + AnyFormattedText, + StyleAndTextTuples, + to_formatted_text, +) +from prompt_toolkit.formatted_text.utils import fragment_list_width +from prompt_toolkit.layout.dimension import AnyDimension, D +from prompt_toolkit.layout.utils import explode_text_fragments +from prompt_toolkit.utils import get_cwidth + +if TYPE_CHECKING: + from .base import ProgressBar, ProgressBarCounter + +__all__ = [ + "Formatter", + "Text", + "Label", + "Percentage", + "Bar", + "Progress", + "TimeElapsed", + "TimeLeft", + "IterationsPerSecond", + "SpinningWheel", + "Rainbow", + "create_default_formatters", +] + + +class Formatter(metaclass=ABCMeta): + """ + Base class for any formatter. + """ + + @abstractmethod + def format( + self, + progress_bar: "ProgressBar", + progress: "ProgressBarCounter[object]", + width: int, + ) -> AnyFormattedText: + pass + + def get_width(self, progress_bar: "ProgressBar") -> AnyDimension: + return D() + + +class Text(Formatter): + """ + Display plain text. + """ + + def __init__(self, text: AnyFormattedText, style: str = "") -> None: + self.text = to_formatted_text(text, style=style) + + def format( + self, + progress_bar: "ProgressBar", + progress: "ProgressBarCounter[object]", + width: int, + ) -> AnyFormattedText: + return self.text + + def get_width(self, progress_bar: "ProgressBar") -> AnyDimension: + return fragment_list_width(self.text) + + +class Label(Formatter): + """ + Display the name of the current task. + + :param width: If a `width` is given, use this width. Scroll the text if it + doesn't fit in this width. + :param suffix: String suffix to be added after the task name, e.g. ': '. + If no task name was given, no suffix will be added. + """ + + def __init__(self, width: AnyDimension = None, suffix: str = "") -> None: + self.width = width + self.suffix = suffix + + def _add_suffix(self, label: AnyFormattedText) -> StyleAndTextTuples: + label = to_formatted_text(label, style="class:label") + return label + [("", self.suffix)] + + def format( + self, + progress_bar: "ProgressBar", + progress: "ProgressBarCounter[object]", + width: int, + ) -> AnyFormattedText: + + label = self._add_suffix(progress.label) + cwidth = fragment_list_width(label) + + if cwidth > width: + # It doesn't fit -> scroll task name. + label = explode_text_fragments(label) + max_scroll = cwidth - width + current_scroll = int(time.time() * 3 % max_scroll) + label = label[current_scroll:] + + return label + + def get_width(self, progress_bar: "ProgressBar") -> AnyDimension: + if self.width: + return self.width + + all_labels = [self._add_suffix(c.label) for c in progress_bar.counters] + if all_labels: + max_widths = max(fragment_list_width(l) for l in all_labels) + return D(preferred=max_widths, max=max_widths) + else: + return D() + + +class Percentage(Formatter): + """ + Display the progress as a percentage. + """ + + template = "<percentage>{percentage:>5}%</percentage>" + + def format( + self, + progress_bar: "ProgressBar", + progress: "ProgressBarCounter[object]", + width: int, + ) -> AnyFormattedText: + + return HTML(self.template).format(percentage=round(progress.percentage, 1)) + + def get_width(self, progress_bar: "ProgressBar") -> AnyDimension: + return D.exact(6) + + +class Bar(Formatter): + """ + Display the progress bar itself. + """ + + template = "<bar>{start}<bar-a>{bar_a}</bar-a><bar-b>{bar_b}</bar-b><bar-c>{bar_c}</bar-c>{end}</bar>" + + def __init__( + self, + start: str = "[", + end: str = "]", + sym_a: str = "=", + sym_b: str = ">", + sym_c: str = " ", + unknown: str = "#", + ) -> None: + + assert len(sym_a) == 1 and get_cwidth(sym_a) == 1 + assert len(sym_c) == 1 and get_cwidth(sym_c) == 1 + + self.start = start + self.end = end + self.sym_a = sym_a + self.sym_b = sym_b + self.sym_c = sym_c + self.unknown = unknown + + def format( + self, + progress_bar: "ProgressBar", + progress: "ProgressBarCounter[object]", + width: int, + ) -> AnyFormattedText: + if progress.done or progress.total or progress.stopped: + sym_a, sym_b, sym_c = self.sym_a, self.sym_b, self.sym_c + + # Compute pb_a based on done, total, or stopped states. + if progress.done: + # 100% completed irrelevant of how much was actually marked as completed. + percent = 1.0 + else: + # Show percentage completed. + percent = progress.percentage / 100 + else: + # Total is unknown and bar is still running. + sym_a, sym_b, sym_c = self.sym_c, self.unknown, self.sym_c + + # Compute percent based on the time. + percent = time.time() * 20 % 100 / 100 + + # Subtract left, sym_b, and right. + width -= get_cwidth(self.start + sym_b + self.end) + + # Scale percent by width + pb_a = int(percent * width) + bar_a = sym_a * pb_a + bar_b = sym_b + bar_c = sym_c * (width - pb_a) + + return HTML(self.template).format( + start=self.start, end=self.end, bar_a=bar_a, bar_b=bar_b, bar_c=bar_c + ) + + def get_width(self, progress_bar: "ProgressBar") -> AnyDimension: + return D(min=9) + + +class Progress(Formatter): + """ + Display the progress as text. E.g. "8/20" + """ + + template = "<current>{current:>3}</current>/<total>{total:>3}</total>" + + def format( + self, + progress_bar: "ProgressBar", + progress: "ProgressBarCounter[object]", + width: int, + ) -> AnyFormattedText: + + return HTML(self.template).format( + current=progress.items_completed, total=progress.total or "?" + ) + + def get_width(self, progress_bar: "ProgressBar") -> AnyDimension: + all_lengths = [ + len("{0:>3}".format(c.total or "?")) for c in progress_bar.counters + ] + all_lengths.append(1) + return D.exact(max(all_lengths) * 2 + 1) + + +def _format_timedelta(timedelta: datetime.timedelta) -> str: + """ + Return hh:mm:ss, or mm:ss if the amount of hours is zero. + """ + result = "{0}".format(timedelta).split(".")[0] + if result.startswith("0:"): + result = result[2:] + return result + + +class TimeElapsed(Formatter): + """ + Display the elapsed time. + """ + + def format( + self, + progress_bar: "ProgressBar", + progress: "ProgressBarCounter[object]", + width: int, + ) -> AnyFormattedText: + + text = _format_timedelta(progress.time_elapsed).rjust(width) + return HTML("<time-elapsed>{time_elapsed}</time-elapsed>").format( + time_elapsed=text + ) + + def get_width(self, progress_bar: "ProgressBar") -> AnyDimension: + all_values = [ + len(_format_timedelta(c.time_elapsed)) for c in progress_bar.counters + ] + if all_values: + return max(all_values) + return 0 + + +class TimeLeft(Formatter): + """ + Display the time left. + """ + + template = "<time-left>{time_left}</time-left>" + unknown = "?:??:??" + + def format( + self, + progress_bar: "ProgressBar", + progress: "ProgressBarCounter[object]", + width: int, + ) -> AnyFormattedText: + + time_left = progress.time_left + if time_left is not None: + formatted_time_left = _format_timedelta(time_left) + else: + formatted_time_left = self.unknown + + return HTML(self.template).format(time_left=formatted_time_left.rjust(width)) + + def get_width(self, progress_bar: "ProgressBar") -> AnyDimension: + all_values = [ + len(_format_timedelta(c.time_left)) if c.time_left is not None else 7 + for c in progress_bar.counters + ] + if all_values: + return max(all_values) + return 0 + + +class IterationsPerSecond(Formatter): + """ + Display the iterations per second. + """ + + template = ( + "<iterations-per-second>{iterations_per_second:.2f}</iterations-per-second>" + ) + + def format( + self, + progress_bar: "ProgressBar", + progress: "ProgressBarCounter[object]", + width: int, + ) -> AnyFormattedText: + + value = progress.items_completed / progress.time_elapsed.total_seconds() + return HTML(self.template.format(iterations_per_second=value)) + + def get_width(self, progress_bar: "ProgressBar") -> AnyDimension: + all_values = [ + len("{0:.2f}".format(c.items_completed / c.time_elapsed.total_seconds())) + for c in progress_bar.counters + ] + if all_values: + return max(all_values) + return 0 + + +class SpinningWheel(Formatter): + """ + Display a spinning wheel. + """ + + characters = r"/-\|" + + def format( + self, + progress_bar: "ProgressBar", + progress: "ProgressBarCounter[object]", + width: int, + ) -> AnyFormattedText: + + index = int(time.time() * 3) % len(self.characters) + return HTML("<spinning-wheel>{0}</spinning-wheel>").format( + self.characters[index] + ) + + def get_width(self, progress_bar: "ProgressBar") -> AnyDimension: + return D.exact(1) + + +def _hue_to_rgb(hue: float) -> Tuple[int, int, int]: + """ + Take hue between 0 and 1, return (r, g, b). + """ + i = int(hue * 6.0) + f = (hue * 6.0) - i + + q = int(255 * (1.0 - f)) + t = int(255 * (1.0 - (1.0 - f))) + + i %= 6 + + return [ + (255, t, 0), + (q, 255, 0), + (0, 255, t), + (0, q, 255), + (t, 0, 255), + (255, 0, q), + ][i] + + +class Rainbow(Formatter): + """ + For the fun. Add rainbow colors to any of the other formatters. + """ + + colors = ["#%.2x%.2x%.2x" % _hue_to_rgb(h / 100.0) for h in range(0, 100)] + + def __init__(self, formatter: Formatter) -> None: + self.formatter = formatter + + def format( + self, + progress_bar: "ProgressBar", + progress: "ProgressBarCounter[object]", + width: int, + ) -> AnyFormattedText: + + # Get formatted text from nested formatter, and explode it in + # text/style tuples. + result = self.formatter.format(progress_bar, progress, width) + result = explode_text_fragments(to_formatted_text(result)) + + # Insert colors. + result2: StyleAndTextTuples = [] + shift = int(time.time() * 3) % len(self.colors) + + for i, (style, text, *_) in enumerate(result): + result2.append( + (style + " " + self.colors[(i + shift) % len(self.colors)], text) + ) + return result2 + + def get_width(self, progress_bar: "ProgressBar") -> AnyDimension: + return self.formatter.get_width(progress_bar) + + +def create_default_formatters() -> List[Formatter]: + """ + Return the list of default formatters. + """ + return [ + Label(), + Text(" "), + Percentage(), + Text(" "), + Bar(), + Text(" "), + Progress(), + Text(" "), + Text("eta [", style="class:time-left"), + TimeLeft(), + Text("]", style="class:time-left"), + Text(" "), + ] diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/prompt.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/prompt.py index 85b85c507d..a8d8a58555 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/prompt.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/prompt.py @@ -1,1498 +1,1498 @@ -""" -Line editing functionality. ---------------------------- - -This provides a UI for a line input, similar to GNU Readline, libedit and -linenoise. - -Either call the `prompt` function for every line input. Or create an instance -of the :class:`.PromptSession` class and call the `prompt` method from that -class. In the second case, we'll have a 'session' that keeps all the state like -the history in between several calls. - -There is a lot of overlap between the arguments taken by the `prompt` function -and the `PromptSession` (like `completer`, `style`, etcetera). There we have -the freedom to decide which settings we want for the whole 'session', and which -we want for an individual `prompt`. - -Example:: - - # Simple `prompt` call. - result = prompt('Say something: ') - - # Using a 'session'. - s = PromptSession() - result = s.prompt('Say something: ') -""" -from contextlib import contextmanager -from enum import Enum -from functools import partial -from typing import ( - TYPE_CHECKING, - Callable, - Generic, - Iterator, - List, - Optional, - Tuple, - TypeVar, - Union, - cast, -) - -from prompt_toolkit.application import Application -from prompt_toolkit.application.current import get_app -from prompt_toolkit.auto_suggest import AutoSuggest, DynamicAutoSuggest -from prompt_toolkit.buffer import Buffer -from prompt_toolkit.clipboard import Clipboard, DynamicClipboard, InMemoryClipboard -from prompt_toolkit.completion import Completer, DynamicCompleter, ThreadedCompleter +""" +Line editing functionality. +--------------------------- + +This provides a UI for a line input, similar to GNU Readline, libedit and +linenoise. + +Either call the `prompt` function for every line input. Or create an instance +of the :class:`.PromptSession` class and call the `prompt` method from that +class. In the second case, we'll have a 'session' that keeps all the state like +the history in between several calls. + +There is a lot of overlap between the arguments taken by the `prompt` function +and the `PromptSession` (like `completer`, `style`, etcetera). There we have +the freedom to decide which settings we want for the whole 'session', and which +we want for an individual `prompt`. + +Example:: + + # Simple `prompt` call. + result = prompt('Say something: ') + + # Using a 'session'. + s = PromptSession() + result = s.prompt('Say something: ') +""" +from contextlib import contextmanager +from enum import Enum +from functools import partial +from typing import ( + TYPE_CHECKING, + Callable, + Generic, + Iterator, + List, + Optional, + Tuple, + TypeVar, + Union, + cast, +) + +from prompt_toolkit.application import Application +from prompt_toolkit.application.current import get_app +from prompt_toolkit.auto_suggest import AutoSuggest, DynamicAutoSuggest +from prompt_toolkit.buffer import Buffer +from prompt_toolkit.clipboard import Clipboard, DynamicClipboard, InMemoryClipboard +from prompt_toolkit.completion import Completer, DynamicCompleter, ThreadedCompleter from prompt_toolkit.cursor_shapes import ( AnyCursorShapeConfig, CursorShapeConfig, DynamicCursorShapeConfig, ) -from prompt_toolkit.document import Document -from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER, EditingMode -from prompt_toolkit.eventloop import get_event_loop -from prompt_toolkit.filters import ( - Condition, - FilterOrBool, - has_arg, - has_focus, - is_done, - is_true, - renderer_height_is_known, - to_filter, -) -from prompt_toolkit.formatted_text import ( - AnyFormattedText, - StyleAndTextTuples, - fragment_list_to_text, - merge_formatted_text, - to_formatted_text, -) -from prompt_toolkit.history import History, InMemoryHistory -from prompt_toolkit.input.base import Input -from prompt_toolkit.key_binding.bindings.auto_suggest import load_auto_suggest_bindings -from prompt_toolkit.key_binding.bindings.completion import ( - display_completions_like_readline, -) -from prompt_toolkit.key_binding.bindings.open_in_editor import ( - load_open_in_editor_bindings, -) -from prompt_toolkit.key_binding.key_bindings import ( - ConditionalKeyBindings, - DynamicKeyBindings, - KeyBindings, - KeyBindingsBase, - merge_key_bindings, -) -from prompt_toolkit.key_binding.key_processor import KeyPressEvent -from prompt_toolkit.keys import Keys -from prompt_toolkit.layout import Float, FloatContainer, HSplit, Window -from prompt_toolkit.layout.containers import ConditionalContainer, WindowAlign -from prompt_toolkit.layout.controls import ( - BufferControl, - FormattedTextControl, - SearchBufferControl, -) -from prompt_toolkit.layout.dimension import Dimension -from prompt_toolkit.layout.layout import Layout -from prompt_toolkit.layout.menus import CompletionsMenu, MultiColumnCompletionsMenu -from prompt_toolkit.layout.processors import ( - AfterInput, - AppendAutoSuggestion, - ConditionalProcessor, - DisplayMultipleCursors, - DynamicProcessor, - HighlightIncrementalSearchProcessor, - HighlightSelectionProcessor, - PasswordProcessor, - Processor, - ReverseSearchProcessor, - merge_processors, -) -from prompt_toolkit.layout.utils import explode_text_fragments -from prompt_toolkit.lexers import DynamicLexer, Lexer -from prompt_toolkit.output import ColorDepth, DummyOutput, Output -from prompt_toolkit.styles import ( - BaseStyle, - ConditionalStyleTransformation, - DynamicStyle, - DynamicStyleTransformation, - StyleTransformation, - SwapLightAndDarkStyleTransformation, - merge_style_transformations, -) -from prompt_toolkit.utils import ( - get_cwidth, - is_dumb_terminal, - suspend_to_background_supported, - to_str, -) -from prompt_toolkit.validation import DynamicValidator, Validator -from prompt_toolkit.widgets.toolbars import ( - SearchToolbar, - SystemToolbar, - ValidationToolbar, -) - -if TYPE_CHECKING: - from prompt_toolkit.formatted_text.base import MagicFormattedText - -__all__ = [ - "PromptSession", - "prompt", - "confirm", - "create_confirm_session", # Used by '_display_completions_like_readline'. - "CompleteStyle", -] - -_StyleAndTextTuplesCallable = Callable[[], StyleAndTextTuples] -E = KeyPressEvent - - -def _split_multiline_prompt( - get_prompt_text: _StyleAndTextTuplesCallable, -) -> Tuple[ - Callable[[], bool], _StyleAndTextTuplesCallable, _StyleAndTextTuplesCallable -]: - """ - Take a `get_prompt_text` function and return three new functions instead. - One that tells whether this prompt consists of multiple lines; one that - returns the fragments to be shown on the lines above the input; and another - one with the fragments to be shown at the first line of the input. - """ - - def has_before_fragments() -> bool: - for fragment, char, *_ in get_prompt_text(): - if "\n" in char: - return True - return False - - def before() -> StyleAndTextTuples: - result: StyleAndTextTuples = [] - found_nl = False - for fragment, char, *_ in reversed(explode_text_fragments(get_prompt_text())): - if found_nl: - result.insert(0, (fragment, char)) - elif char == "\n": - found_nl = True - return result - - def first_input_line() -> StyleAndTextTuples: - result: StyleAndTextTuples = [] - for fragment, char, *_ in reversed(explode_text_fragments(get_prompt_text())): - if char == "\n": - break - else: - result.insert(0, (fragment, char)) - return result - - return has_before_fragments, before, first_input_line - - -class _RPrompt(Window): - """ - The prompt that is displayed on the right side of the Window. - """ - - def __init__(self, text: AnyFormattedText) -> None: - super().__init__( - FormattedTextControl(text=text), - align=WindowAlign.RIGHT, - style="class:rprompt", - ) - - -class CompleteStyle(str, Enum): - """ - How to display autocompletions for the prompt. - """ - - value: str - - COLUMN = "COLUMN" - MULTI_COLUMN = "MULTI_COLUMN" - READLINE_LIKE = "READLINE_LIKE" - - -# Formatted text for the continuation prompt. It's the same like other -# formatted text, except that if it's a callable, it takes three arguments. -PromptContinuationText = Union[ - str, - "MagicFormattedText", - StyleAndTextTuples, - # (prompt_width, line_number, wrap_count) -> AnyFormattedText. - Callable[[int, int, int], AnyFormattedText], -] - -_T = TypeVar("_T") - - -class PromptSession(Generic[_T]): - """ - PromptSession for a prompt application, which can be used as a GNU Readline - replacement. - - This is a wrapper around a lot of ``prompt_toolkit`` functionality and can - be a replacement for `raw_input`. - - All parameters that expect "formatted text" can take either just plain text - (a unicode object), a list of ``(style_str, text)`` tuples or an HTML object. - - Example usage:: - - s = PromptSession(message='>') - text = s.prompt() - - :param message: Plain text or formatted text to be shown before the prompt. - This can also be a callable that returns formatted text. - :param multiline: `bool` or :class:`~prompt_toolkit.filters.Filter`. - When True, prefer a layout that is more adapted for multiline input. - Text after newlines is automatically indented, and search/arg input is - shown below the input, instead of replacing the prompt. - :param wrap_lines: `bool` or :class:`~prompt_toolkit.filters.Filter`. - When True (the default), automatically wrap long lines instead of - scrolling horizontally. - :param is_password: Show asterisks instead of the actual typed characters. - :param editing_mode: ``EditingMode.VI`` or ``EditingMode.EMACS``. - :param vi_mode: `bool`, if True, Identical to ``editing_mode=EditingMode.VI``. - :param complete_while_typing: `bool` or - :class:`~prompt_toolkit.filters.Filter`. Enable autocompletion while - typing. - :param validate_while_typing: `bool` or - :class:`~prompt_toolkit.filters.Filter`. Enable input validation while - typing. - :param enable_history_search: `bool` or - :class:`~prompt_toolkit.filters.Filter`. Enable up-arrow parting - string matching. - :param search_ignore_case: - :class:`~prompt_toolkit.filters.Filter`. Search case insensitive. - :param lexer: :class:`~prompt_toolkit.lexers.Lexer` to be used for the - syntax highlighting. - :param validator: :class:`~prompt_toolkit.validation.Validator` instance - for input validation. - :param completer: :class:`~prompt_toolkit.completion.Completer` instance - for input completion. - :param complete_in_thread: `bool` or - :class:`~prompt_toolkit.filters.Filter`. Run the completer code in a - background thread in order to avoid blocking the user interface. - For ``CompleteStyle.READLINE_LIKE``, this setting has no effect. There - we always run the completions in the main thread. - :param reserve_space_for_menu: Space to be reserved for displaying the menu. - (0 means that no space needs to be reserved.) - :param auto_suggest: :class:`~prompt_toolkit.auto_suggest.AutoSuggest` - instance for input suggestions. - :param style: :class:`.Style` instance for the color scheme. - :param include_default_pygments_style: `bool` or - :class:`~prompt_toolkit.filters.Filter`. Tell whether the default - styling for Pygments lexers has to be included. By default, this is - true, but it is recommended to be disabled if another Pygments style is - passed as the `style` argument, otherwise, two Pygments styles will be - merged. - :param style_transformation: - :class:`~prompt_toolkit.style.StyleTransformation` instance. - :param swap_light_and_dark_colors: `bool` or - :class:`~prompt_toolkit.filters.Filter`. When enabled, apply - :class:`~prompt_toolkit.style.SwapLightAndDarkStyleTransformation`. - This is useful for switching between dark and light terminal - backgrounds. - :param enable_system_prompt: `bool` or - :class:`~prompt_toolkit.filters.Filter`. Pressing Meta+'!' will show - a system prompt. - :param enable_suspend: `bool` or :class:`~prompt_toolkit.filters.Filter`. - Enable Control-Z style suspension. - :param enable_open_in_editor: `bool` or - :class:`~prompt_toolkit.filters.Filter`. Pressing 'v' in Vi mode or - C-X C-E in emacs mode will open an external editor. - :param history: :class:`~prompt_toolkit.history.History` instance. - :param clipboard: :class:`~prompt_toolkit.clipboard.Clipboard` instance. - (e.g. :class:`~prompt_toolkit.clipboard.InMemoryClipboard`) - :param rprompt: Text or formatted text to be displayed on the right side. - This can also be a callable that returns (formatted) text. - :param bottom_toolbar: Formatted text or callable which is supposed to - return formatted text. - :param prompt_continuation: Text that needs to be displayed for a multiline - prompt continuation. This can either be formatted text or a callable - that takes a `prompt_width`, `line_number` and `wrap_count` as input - and returns formatted text. When this is `None` (the default), then - `prompt_width` spaces will be used. - :param complete_style: ``CompleteStyle.COLUMN``, - ``CompleteStyle.MULTI_COLUMN`` or ``CompleteStyle.READLINE_LIKE``. - :param mouse_support: `bool` or :class:`~prompt_toolkit.filters.Filter` - to enable mouse support. - :param placeholder: Text to be displayed when no input has been given - yet. Unlike the `default` parameter, this won't be returned as part of - the output ever. This can be formatted text or a callable that returns - formatted text. - :param refresh_interval: (number; in seconds) When given, refresh the UI - every so many seconds. - :param input: `Input` object. (Note that the preferred way to change the - input/output is by creating an `AppSession`.) - :param output: `Output` object. - """ - - _fields = ( - "message", - "lexer", - "completer", - "complete_in_thread", - "is_password", - "editing_mode", - "key_bindings", - "is_password", - "bottom_toolbar", - "style", - "style_transformation", - "swap_light_and_dark_colors", - "color_depth", +from prompt_toolkit.document import Document +from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER, EditingMode +from prompt_toolkit.eventloop import get_event_loop +from prompt_toolkit.filters import ( + Condition, + FilterOrBool, + has_arg, + has_focus, + is_done, + is_true, + renderer_height_is_known, + to_filter, +) +from prompt_toolkit.formatted_text import ( + AnyFormattedText, + StyleAndTextTuples, + fragment_list_to_text, + merge_formatted_text, + to_formatted_text, +) +from prompt_toolkit.history import History, InMemoryHistory +from prompt_toolkit.input.base import Input +from prompt_toolkit.key_binding.bindings.auto_suggest import load_auto_suggest_bindings +from prompt_toolkit.key_binding.bindings.completion import ( + display_completions_like_readline, +) +from prompt_toolkit.key_binding.bindings.open_in_editor import ( + load_open_in_editor_bindings, +) +from prompt_toolkit.key_binding.key_bindings import ( + ConditionalKeyBindings, + DynamicKeyBindings, + KeyBindings, + KeyBindingsBase, + merge_key_bindings, +) +from prompt_toolkit.key_binding.key_processor import KeyPressEvent +from prompt_toolkit.keys import Keys +from prompt_toolkit.layout import Float, FloatContainer, HSplit, Window +from prompt_toolkit.layout.containers import ConditionalContainer, WindowAlign +from prompt_toolkit.layout.controls import ( + BufferControl, + FormattedTextControl, + SearchBufferControl, +) +from prompt_toolkit.layout.dimension import Dimension +from prompt_toolkit.layout.layout import Layout +from prompt_toolkit.layout.menus import CompletionsMenu, MultiColumnCompletionsMenu +from prompt_toolkit.layout.processors import ( + AfterInput, + AppendAutoSuggestion, + ConditionalProcessor, + DisplayMultipleCursors, + DynamicProcessor, + HighlightIncrementalSearchProcessor, + HighlightSelectionProcessor, + PasswordProcessor, + Processor, + ReverseSearchProcessor, + merge_processors, +) +from prompt_toolkit.layout.utils import explode_text_fragments +from prompt_toolkit.lexers import DynamicLexer, Lexer +from prompt_toolkit.output import ColorDepth, DummyOutput, Output +from prompt_toolkit.styles import ( + BaseStyle, + ConditionalStyleTransformation, + DynamicStyle, + DynamicStyleTransformation, + StyleTransformation, + SwapLightAndDarkStyleTransformation, + merge_style_transformations, +) +from prompt_toolkit.utils import ( + get_cwidth, + is_dumb_terminal, + suspend_to_background_supported, + to_str, +) +from prompt_toolkit.validation import DynamicValidator, Validator +from prompt_toolkit.widgets.toolbars import ( + SearchToolbar, + SystemToolbar, + ValidationToolbar, +) + +if TYPE_CHECKING: + from prompt_toolkit.formatted_text.base import MagicFormattedText + +__all__ = [ + "PromptSession", + "prompt", + "confirm", + "create_confirm_session", # Used by '_display_completions_like_readline'. + "CompleteStyle", +] + +_StyleAndTextTuplesCallable = Callable[[], StyleAndTextTuples] +E = KeyPressEvent + + +def _split_multiline_prompt( + get_prompt_text: _StyleAndTextTuplesCallable, +) -> Tuple[ + Callable[[], bool], _StyleAndTextTuplesCallable, _StyleAndTextTuplesCallable +]: + """ + Take a `get_prompt_text` function and return three new functions instead. + One that tells whether this prompt consists of multiple lines; one that + returns the fragments to be shown on the lines above the input; and another + one with the fragments to be shown at the first line of the input. + """ + + def has_before_fragments() -> bool: + for fragment, char, *_ in get_prompt_text(): + if "\n" in char: + return True + return False + + def before() -> StyleAndTextTuples: + result: StyleAndTextTuples = [] + found_nl = False + for fragment, char, *_ in reversed(explode_text_fragments(get_prompt_text())): + if found_nl: + result.insert(0, (fragment, char)) + elif char == "\n": + found_nl = True + return result + + def first_input_line() -> StyleAndTextTuples: + result: StyleAndTextTuples = [] + for fragment, char, *_ in reversed(explode_text_fragments(get_prompt_text())): + if char == "\n": + break + else: + result.insert(0, (fragment, char)) + return result + + return has_before_fragments, before, first_input_line + + +class _RPrompt(Window): + """ + The prompt that is displayed on the right side of the Window. + """ + + def __init__(self, text: AnyFormattedText) -> None: + super().__init__( + FormattedTextControl(text=text), + align=WindowAlign.RIGHT, + style="class:rprompt", + ) + + +class CompleteStyle(str, Enum): + """ + How to display autocompletions for the prompt. + """ + + value: str + + COLUMN = "COLUMN" + MULTI_COLUMN = "MULTI_COLUMN" + READLINE_LIKE = "READLINE_LIKE" + + +# Formatted text for the continuation prompt. It's the same like other +# formatted text, except that if it's a callable, it takes three arguments. +PromptContinuationText = Union[ + str, + "MagicFormattedText", + StyleAndTextTuples, + # (prompt_width, line_number, wrap_count) -> AnyFormattedText. + Callable[[int, int, int], AnyFormattedText], +] + +_T = TypeVar("_T") + + +class PromptSession(Generic[_T]): + """ + PromptSession for a prompt application, which can be used as a GNU Readline + replacement. + + This is a wrapper around a lot of ``prompt_toolkit`` functionality and can + be a replacement for `raw_input`. + + All parameters that expect "formatted text" can take either just plain text + (a unicode object), a list of ``(style_str, text)`` tuples or an HTML object. + + Example usage:: + + s = PromptSession(message='>') + text = s.prompt() + + :param message: Plain text or formatted text to be shown before the prompt. + This can also be a callable that returns formatted text. + :param multiline: `bool` or :class:`~prompt_toolkit.filters.Filter`. + When True, prefer a layout that is more adapted for multiline input. + Text after newlines is automatically indented, and search/arg input is + shown below the input, instead of replacing the prompt. + :param wrap_lines: `bool` or :class:`~prompt_toolkit.filters.Filter`. + When True (the default), automatically wrap long lines instead of + scrolling horizontally. + :param is_password: Show asterisks instead of the actual typed characters. + :param editing_mode: ``EditingMode.VI`` or ``EditingMode.EMACS``. + :param vi_mode: `bool`, if True, Identical to ``editing_mode=EditingMode.VI``. + :param complete_while_typing: `bool` or + :class:`~prompt_toolkit.filters.Filter`. Enable autocompletion while + typing. + :param validate_while_typing: `bool` or + :class:`~prompt_toolkit.filters.Filter`. Enable input validation while + typing. + :param enable_history_search: `bool` or + :class:`~prompt_toolkit.filters.Filter`. Enable up-arrow parting + string matching. + :param search_ignore_case: + :class:`~prompt_toolkit.filters.Filter`. Search case insensitive. + :param lexer: :class:`~prompt_toolkit.lexers.Lexer` to be used for the + syntax highlighting. + :param validator: :class:`~prompt_toolkit.validation.Validator` instance + for input validation. + :param completer: :class:`~prompt_toolkit.completion.Completer` instance + for input completion. + :param complete_in_thread: `bool` or + :class:`~prompt_toolkit.filters.Filter`. Run the completer code in a + background thread in order to avoid blocking the user interface. + For ``CompleteStyle.READLINE_LIKE``, this setting has no effect. There + we always run the completions in the main thread. + :param reserve_space_for_menu: Space to be reserved for displaying the menu. + (0 means that no space needs to be reserved.) + :param auto_suggest: :class:`~prompt_toolkit.auto_suggest.AutoSuggest` + instance for input suggestions. + :param style: :class:`.Style` instance for the color scheme. + :param include_default_pygments_style: `bool` or + :class:`~prompt_toolkit.filters.Filter`. Tell whether the default + styling for Pygments lexers has to be included. By default, this is + true, but it is recommended to be disabled if another Pygments style is + passed as the `style` argument, otherwise, two Pygments styles will be + merged. + :param style_transformation: + :class:`~prompt_toolkit.style.StyleTransformation` instance. + :param swap_light_and_dark_colors: `bool` or + :class:`~prompt_toolkit.filters.Filter`. When enabled, apply + :class:`~prompt_toolkit.style.SwapLightAndDarkStyleTransformation`. + This is useful for switching between dark and light terminal + backgrounds. + :param enable_system_prompt: `bool` or + :class:`~prompt_toolkit.filters.Filter`. Pressing Meta+'!' will show + a system prompt. + :param enable_suspend: `bool` or :class:`~prompt_toolkit.filters.Filter`. + Enable Control-Z style suspension. + :param enable_open_in_editor: `bool` or + :class:`~prompt_toolkit.filters.Filter`. Pressing 'v' in Vi mode or + C-X C-E in emacs mode will open an external editor. + :param history: :class:`~prompt_toolkit.history.History` instance. + :param clipboard: :class:`~prompt_toolkit.clipboard.Clipboard` instance. + (e.g. :class:`~prompt_toolkit.clipboard.InMemoryClipboard`) + :param rprompt: Text or formatted text to be displayed on the right side. + This can also be a callable that returns (formatted) text. + :param bottom_toolbar: Formatted text or callable which is supposed to + return formatted text. + :param prompt_continuation: Text that needs to be displayed for a multiline + prompt continuation. This can either be formatted text or a callable + that takes a `prompt_width`, `line_number` and `wrap_count` as input + and returns formatted text. When this is `None` (the default), then + `prompt_width` spaces will be used. + :param complete_style: ``CompleteStyle.COLUMN``, + ``CompleteStyle.MULTI_COLUMN`` or ``CompleteStyle.READLINE_LIKE``. + :param mouse_support: `bool` or :class:`~prompt_toolkit.filters.Filter` + to enable mouse support. + :param placeholder: Text to be displayed when no input has been given + yet. Unlike the `default` parameter, this won't be returned as part of + the output ever. This can be formatted text or a callable that returns + formatted text. + :param refresh_interval: (number; in seconds) When given, refresh the UI + every so many seconds. + :param input: `Input` object. (Note that the preferred way to change the + input/output is by creating an `AppSession`.) + :param output: `Output` object. + """ + + _fields = ( + "message", + "lexer", + "completer", + "complete_in_thread", + "is_password", + "editing_mode", + "key_bindings", + "is_password", + "bottom_toolbar", + "style", + "style_transformation", + "swap_light_and_dark_colors", + "color_depth", "cursor", - "include_default_pygments_style", - "rprompt", - "multiline", - "prompt_continuation", - "wrap_lines", - "enable_history_search", - "search_ignore_case", - "complete_while_typing", - "validate_while_typing", - "complete_style", - "mouse_support", - "auto_suggest", - "clipboard", - "validator", - "refresh_interval", - "input_processors", - "placeholder", - "enable_system_prompt", - "enable_suspend", - "enable_open_in_editor", - "reserve_space_for_menu", - "tempfile_suffix", - "tempfile", - ) - - def __init__( - self, - message: AnyFormattedText = "", - *, - multiline: FilterOrBool = False, - wrap_lines: FilterOrBool = True, - is_password: FilterOrBool = False, - vi_mode: bool = False, - editing_mode: EditingMode = EditingMode.EMACS, - complete_while_typing: FilterOrBool = True, - validate_while_typing: FilterOrBool = True, - enable_history_search: FilterOrBool = False, - search_ignore_case: FilterOrBool = False, - lexer: Optional[Lexer] = None, - enable_system_prompt: FilterOrBool = False, - enable_suspend: FilterOrBool = False, - enable_open_in_editor: FilterOrBool = False, - validator: Optional[Validator] = None, - completer: Optional[Completer] = None, - complete_in_thread: bool = False, - reserve_space_for_menu: int = 8, - complete_style: CompleteStyle = CompleteStyle.COLUMN, - auto_suggest: Optional[AutoSuggest] = None, - style: Optional[BaseStyle] = None, - style_transformation: Optional[StyleTransformation] = None, - swap_light_and_dark_colors: FilterOrBool = False, - color_depth: Optional[ColorDepth] = None, + "include_default_pygments_style", + "rprompt", + "multiline", + "prompt_continuation", + "wrap_lines", + "enable_history_search", + "search_ignore_case", + "complete_while_typing", + "validate_while_typing", + "complete_style", + "mouse_support", + "auto_suggest", + "clipboard", + "validator", + "refresh_interval", + "input_processors", + "placeholder", + "enable_system_prompt", + "enable_suspend", + "enable_open_in_editor", + "reserve_space_for_menu", + "tempfile_suffix", + "tempfile", + ) + + def __init__( + self, + message: AnyFormattedText = "", + *, + multiline: FilterOrBool = False, + wrap_lines: FilterOrBool = True, + is_password: FilterOrBool = False, + vi_mode: bool = False, + editing_mode: EditingMode = EditingMode.EMACS, + complete_while_typing: FilterOrBool = True, + validate_while_typing: FilterOrBool = True, + enable_history_search: FilterOrBool = False, + search_ignore_case: FilterOrBool = False, + lexer: Optional[Lexer] = None, + enable_system_prompt: FilterOrBool = False, + enable_suspend: FilterOrBool = False, + enable_open_in_editor: FilterOrBool = False, + validator: Optional[Validator] = None, + completer: Optional[Completer] = None, + complete_in_thread: bool = False, + reserve_space_for_menu: int = 8, + complete_style: CompleteStyle = CompleteStyle.COLUMN, + auto_suggest: Optional[AutoSuggest] = None, + style: Optional[BaseStyle] = None, + style_transformation: Optional[StyleTransformation] = None, + swap_light_and_dark_colors: FilterOrBool = False, + color_depth: Optional[ColorDepth] = None, cursor: AnyCursorShapeConfig = None, - include_default_pygments_style: FilterOrBool = True, - history: Optional[History] = None, - clipboard: Optional[Clipboard] = None, - prompt_continuation: Optional[PromptContinuationText] = None, - rprompt: AnyFormattedText = None, - bottom_toolbar: AnyFormattedText = None, - mouse_support: FilterOrBool = False, - input_processors: Optional[List[Processor]] = None, - placeholder: Optional[AnyFormattedText] = None, - key_bindings: Optional[KeyBindingsBase] = None, - erase_when_done: bool = False, - tempfile_suffix: Optional[Union[str, Callable[[], str]]] = ".txt", - tempfile: Optional[Union[str, Callable[[], str]]] = None, - refresh_interval: float = 0, - input: Optional[Input] = None, - output: Optional[Output] = None, - ) -> None: - - history = history or InMemoryHistory() - clipboard = clipboard or InMemoryClipboard() - - # Ensure backwards-compatibility, when `vi_mode` is passed. - if vi_mode: - editing_mode = EditingMode.VI - - # Store all settings in this class. - self._input = input - self._output = output - - # Store attributes. - # (All except 'editing_mode'.) - self.message = message - self.lexer = lexer - self.completer = completer - self.complete_in_thread = complete_in_thread - self.is_password = is_password - self.key_bindings = key_bindings - self.bottom_toolbar = bottom_toolbar - self.style = style - self.style_transformation = style_transformation - self.swap_light_and_dark_colors = swap_light_and_dark_colors - self.color_depth = color_depth + include_default_pygments_style: FilterOrBool = True, + history: Optional[History] = None, + clipboard: Optional[Clipboard] = None, + prompt_continuation: Optional[PromptContinuationText] = None, + rprompt: AnyFormattedText = None, + bottom_toolbar: AnyFormattedText = None, + mouse_support: FilterOrBool = False, + input_processors: Optional[List[Processor]] = None, + placeholder: Optional[AnyFormattedText] = None, + key_bindings: Optional[KeyBindingsBase] = None, + erase_when_done: bool = False, + tempfile_suffix: Optional[Union[str, Callable[[], str]]] = ".txt", + tempfile: Optional[Union[str, Callable[[], str]]] = None, + refresh_interval: float = 0, + input: Optional[Input] = None, + output: Optional[Output] = None, + ) -> None: + + history = history or InMemoryHistory() + clipboard = clipboard or InMemoryClipboard() + + # Ensure backwards-compatibility, when `vi_mode` is passed. + if vi_mode: + editing_mode = EditingMode.VI + + # Store all settings in this class. + self._input = input + self._output = output + + # Store attributes. + # (All except 'editing_mode'.) + self.message = message + self.lexer = lexer + self.completer = completer + self.complete_in_thread = complete_in_thread + self.is_password = is_password + self.key_bindings = key_bindings + self.bottom_toolbar = bottom_toolbar + self.style = style + self.style_transformation = style_transformation + self.swap_light_and_dark_colors = swap_light_and_dark_colors + self.color_depth = color_depth self.cursor = cursor - self.include_default_pygments_style = include_default_pygments_style - self.rprompt = rprompt - self.multiline = multiline - self.prompt_continuation = prompt_continuation - self.wrap_lines = wrap_lines - self.enable_history_search = enable_history_search - self.search_ignore_case = search_ignore_case - self.complete_while_typing = complete_while_typing - self.validate_while_typing = validate_while_typing - self.complete_style = complete_style - self.mouse_support = mouse_support - self.auto_suggest = auto_suggest - self.clipboard = clipboard - self.validator = validator - self.refresh_interval = refresh_interval - self.input_processors = input_processors - self.placeholder = placeholder - self.enable_system_prompt = enable_system_prompt - self.enable_suspend = enable_suspend - self.enable_open_in_editor = enable_open_in_editor - self.reserve_space_for_menu = reserve_space_for_menu - self.tempfile_suffix = tempfile_suffix - self.tempfile = tempfile - - # Create buffers, layout and Application. - self.history = history - self.default_buffer = self._create_default_buffer() - self.search_buffer = self._create_search_buffer() - self.layout = self._create_layout() - self.app = self._create_application(editing_mode, erase_when_done) - - def _dyncond(self, attr_name: str) -> Condition: - """ - Dynamically take this setting from this 'PromptSession' class. - `attr_name` represents an attribute name of this class. Its value - can either be a boolean or a `Filter`. - - This returns something that can be used as either a `Filter` - or `Filter`. - """ - - @Condition - def dynamic() -> bool: - value = cast(FilterOrBool, getattr(self, attr_name)) - return to_filter(value)() - - return dynamic - - def _create_default_buffer(self) -> Buffer: - """ - Create and return the default input buffer. - """ - dyncond = self._dyncond - - # Create buffers list. - def accept(buff: Buffer) -> bool: - """Accept the content of the default buffer. This is called when - the validation succeeds.""" - cast(Application[str], get_app()).exit(result=buff.document.text) - return True # Keep text, we call 'reset' later on. - - return Buffer( - name=DEFAULT_BUFFER, - # Make sure that complete_while_typing is disabled when - # enable_history_search is enabled. (First convert to Filter, - # to avoid doing bitwise operations on bool objects.) - complete_while_typing=Condition( - lambda: is_true(self.complete_while_typing) - and not is_true(self.enable_history_search) - and not self.complete_style == CompleteStyle.READLINE_LIKE - ), - validate_while_typing=dyncond("validate_while_typing"), - enable_history_search=dyncond("enable_history_search"), - validator=DynamicValidator(lambda: self.validator), - completer=DynamicCompleter( - lambda: ThreadedCompleter(self.completer) - if self.complete_in_thread and self.completer - else self.completer - ), - history=self.history, - auto_suggest=DynamicAutoSuggest(lambda: self.auto_suggest), - accept_handler=accept, - tempfile_suffix=lambda: to_str(self.tempfile_suffix or ""), - tempfile=lambda: to_str(self.tempfile or ""), - ) - - def _create_search_buffer(self) -> Buffer: - return Buffer(name=SEARCH_BUFFER) - - def _create_layout(self) -> Layout: - """ - Create `Layout` for this prompt. - """ - dyncond = self._dyncond - - # Create functions that will dynamically split the prompt. (If we have - # a multiline prompt.) - ( - has_before_fragments, - get_prompt_text_1, - get_prompt_text_2, - ) = _split_multiline_prompt(self._get_prompt) - - default_buffer = self.default_buffer - search_buffer = self.search_buffer - - # Create processors list. - @Condition - def display_placeholder() -> bool: - return self.placeholder is not None and self.default_buffer.text == "" - - all_input_processors = [ - HighlightIncrementalSearchProcessor(), - HighlightSelectionProcessor(), - ConditionalProcessor( - AppendAutoSuggestion(), has_focus(default_buffer) & ~is_done - ), - ConditionalProcessor(PasswordProcessor(), dyncond("is_password")), - DisplayMultipleCursors(), - # Users can insert processors here. - DynamicProcessor(lambda: merge_processors(self.input_processors or [])), - ConditionalProcessor( - AfterInput(lambda: self.placeholder), - filter=display_placeholder, - ), - ] - - # Create bottom toolbars. - bottom_toolbar = ConditionalContainer( - Window( - FormattedTextControl( - lambda: self.bottom_toolbar, style="class:bottom-toolbar.text" - ), - style="class:bottom-toolbar", - dont_extend_height=True, - height=Dimension(min=1), - ), - filter=~is_done - & renderer_height_is_known - & Condition(lambda: self.bottom_toolbar is not None), - ) - - search_toolbar = SearchToolbar( - search_buffer, ignore_case=dyncond("search_ignore_case") - ) - - search_buffer_control = SearchBufferControl( - buffer=search_buffer, - input_processors=[ReverseSearchProcessor()], - ignore_case=dyncond("search_ignore_case"), - ) - - system_toolbar = SystemToolbar( - enable_global_bindings=dyncond("enable_system_prompt") - ) - - def get_search_buffer_control() -> SearchBufferControl: - "Return the UIControl to be focused when searching start." - if is_true(self.multiline): - return search_toolbar.control - else: - return search_buffer_control - - default_buffer_control = BufferControl( - buffer=default_buffer, - search_buffer_control=get_search_buffer_control, - input_processors=all_input_processors, - include_default_input_processors=False, - lexer=DynamicLexer(lambda: self.lexer), - preview_search=True, - ) - - default_buffer_window = Window( - default_buffer_control, - height=self._get_default_buffer_control_height, - get_line_prefix=partial( - self._get_line_prefix, get_prompt_text_2=get_prompt_text_2 - ), - wrap_lines=dyncond("wrap_lines"), - ) - - @Condition - def multi_column_complete_style() -> bool: - return self.complete_style == CompleteStyle.MULTI_COLUMN - - # Build the layout. - layout = HSplit( - [ - # The main input, with completion menus floating on top of it. - FloatContainer( - HSplit( - [ - ConditionalContainer( - Window( - FormattedTextControl(get_prompt_text_1), - dont_extend_height=True, - ), - Condition(has_before_fragments), - ), - ConditionalContainer( - default_buffer_window, - Condition( - lambda: get_app().layout.current_control - != search_buffer_control - ), - ), - ConditionalContainer( - Window(search_buffer_control), - Condition( - lambda: get_app().layout.current_control - == search_buffer_control - ), - ), - ] - ), - [ - # Completion menus. - # NOTE: Especially the multi-column menu needs to be - # transparent, because the shape is not always - # rectangular due to the meta-text below the menu. - Float( - xcursor=True, - ycursor=True, - transparent=True, - content=CompletionsMenu( - max_height=16, - scroll_offset=1, - extra_filter=has_focus(default_buffer) - & ~multi_column_complete_style, - ), - ), - Float( - xcursor=True, - ycursor=True, - transparent=True, - content=MultiColumnCompletionsMenu( - show_meta=True, - extra_filter=has_focus(default_buffer) - & multi_column_complete_style, - ), - ), - # The right prompt. - Float( - right=0, - bottom=0, - hide_when_covering_content=True, - content=_RPrompt(lambda: self.rprompt), - ), - ], - ), - ConditionalContainer(ValidationToolbar(), filter=~is_done), - ConditionalContainer( - system_toolbar, dyncond("enable_system_prompt") & ~is_done - ), - # In multiline mode, we use two toolbars for 'arg' and 'search'. - ConditionalContainer( - Window(FormattedTextControl(self._get_arg_text), height=1), - dyncond("multiline") & has_arg, - ), - ConditionalContainer(search_toolbar, dyncond("multiline") & ~is_done), - bottom_toolbar, - ] - ) - - return Layout(layout, default_buffer_window) - - def _create_application( - self, editing_mode: EditingMode, erase_when_done: bool - ) -> Application[_T]: - """ - Create the `Application` object. - """ - dyncond = self._dyncond - - # Default key bindings. - auto_suggest_bindings = load_auto_suggest_bindings() - open_in_editor_bindings = load_open_in_editor_bindings() - prompt_bindings = self._create_prompt_bindings() - - # Create application - application: Application[_T] = Application( - layout=self.layout, - style=DynamicStyle(lambda: self.style), - style_transformation=merge_style_transformations( - [ - DynamicStyleTransformation(lambda: self.style_transformation), - ConditionalStyleTransformation( - SwapLightAndDarkStyleTransformation(), - dyncond("swap_light_and_dark_colors"), - ), - ] - ), - include_default_pygments_style=dyncond("include_default_pygments_style"), - clipboard=DynamicClipboard(lambda: self.clipboard), - key_bindings=merge_key_bindings( - [ - merge_key_bindings( - [ - auto_suggest_bindings, - ConditionalKeyBindings( - open_in_editor_bindings, - dyncond("enable_open_in_editor") - & has_focus(DEFAULT_BUFFER), - ), - prompt_bindings, - ] - ), - DynamicKeyBindings(lambda: self.key_bindings), - ] - ), - mouse_support=dyncond("mouse_support"), - editing_mode=editing_mode, - erase_when_done=erase_when_done, - reverse_vi_search_direction=True, - color_depth=lambda: self.color_depth, + self.include_default_pygments_style = include_default_pygments_style + self.rprompt = rprompt + self.multiline = multiline + self.prompt_continuation = prompt_continuation + self.wrap_lines = wrap_lines + self.enable_history_search = enable_history_search + self.search_ignore_case = search_ignore_case + self.complete_while_typing = complete_while_typing + self.validate_while_typing = validate_while_typing + self.complete_style = complete_style + self.mouse_support = mouse_support + self.auto_suggest = auto_suggest + self.clipboard = clipboard + self.validator = validator + self.refresh_interval = refresh_interval + self.input_processors = input_processors + self.placeholder = placeholder + self.enable_system_prompt = enable_system_prompt + self.enable_suspend = enable_suspend + self.enable_open_in_editor = enable_open_in_editor + self.reserve_space_for_menu = reserve_space_for_menu + self.tempfile_suffix = tempfile_suffix + self.tempfile = tempfile + + # Create buffers, layout and Application. + self.history = history + self.default_buffer = self._create_default_buffer() + self.search_buffer = self._create_search_buffer() + self.layout = self._create_layout() + self.app = self._create_application(editing_mode, erase_when_done) + + def _dyncond(self, attr_name: str) -> Condition: + """ + Dynamically take this setting from this 'PromptSession' class. + `attr_name` represents an attribute name of this class. Its value + can either be a boolean or a `Filter`. + + This returns something that can be used as either a `Filter` + or `Filter`. + """ + + @Condition + def dynamic() -> bool: + value = cast(FilterOrBool, getattr(self, attr_name)) + return to_filter(value)() + + return dynamic + + def _create_default_buffer(self) -> Buffer: + """ + Create and return the default input buffer. + """ + dyncond = self._dyncond + + # Create buffers list. + def accept(buff: Buffer) -> bool: + """Accept the content of the default buffer. This is called when + the validation succeeds.""" + cast(Application[str], get_app()).exit(result=buff.document.text) + return True # Keep text, we call 'reset' later on. + + return Buffer( + name=DEFAULT_BUFFER, + # Make sure that complete_while_typing is disabled when + # enable_history_search is enabled. (First convert to Filter, + # to avoid doing bitwise operations on bool objects.) + complete_while_typing=Condition( + lambda: is_true(self.complete_while_typing) + and not is_true(self.enable_history_search) + and not self.complete_style == CompleteStyle.READLINE_LIKE + ), + validate_while_typing=dyncond("validate_while_typing"), + enable_history_search=dyncond("enable_history_search"), + validator=DynamicValidator(lambda: self.validator), + completer=DynamicCompleter( + lambda: ThreadedCompleter(self.completer) + if self.complete_in_thread and self.completer + else self.completer + ), + history=self.history, + auto_suggest=DynamicAutoSuggest(lambda: self.auto_suggest), + accept_handler=accept, + tempfile_suffix=lambda: to_str(self.tempfile_suffix or ""), + tempfile=lambda: to_str(self.tempfile or ""), + ) + + def _create_search_buffer(self) -> Buffer: + return Buffer(name=SEARCH_BUFFER) + + def _create_layout(self) -> Layout: + """ + Create `Layout` for this prompt. + """ + dyncond = self._dyncond + + # Create functions that will dynamically split the prompt. (If we have + # a multiline prompt.) + ( + has_before_fragments, + get_prompt_text_1, + get_prompt_text_2, + ) = _split_multiline_prompt(self._get_prompt) + + default_buffer = self.default_buffer + search_buffer = self.search_buffer + + # Create processors list. + @Condition + def display_placeholder() -> bool: + return self.placeholder is not None and self.default_buffer.text == "" + + all_input_processors = [ + HighlightIncrementalSearchProcessor(), + HighlightSelectionProcessor(), + ConditionalProcessor( + AppendAutoSuggestion(), has_focus(default_buffer) & ~is_done + ), + ConditionalProcessor(PasswordProcessor(), dyncond("is_password")), + DisplayMultipleCursors(), + # Users can insert processors here. + DynamicProcessor(lambda: merge_processors(self.input_processors or [])), + ConditionalProcessor( + AfterInput(lambda: self.placeholder), + filter=display_placeholder, + ), + ] + + # Create bottom toolbars. + bottom_toolbar = ConditionalContainer( + Window( + FormattedTextControl( + lambda: self.bottom_toolbar, style="class:bottom-toolbar.text" + ), + style="class:bottom-toolbar", + dont_extend_height=True, + height=Dimension(min=1), + ), + filter=~is_done + & renderer_height_is_known + & Condition(lambda: self.bottom_toolbar is not None), + ) + + search_toolbar = SearchToolbar( + search_buffer, ignore_case=dyncond("search_ignore_case") + ) + + search_buffer_control = SearchBufferControl( + buffer=search_buffer, + input_processors=[ReverseSearchProcessor()], + ignore_case=dyncond("search_ignore_case"), + ) + + system_toolbar = SystemToolbar( + enable_global_bindings=dyncond("enable_system_prompt") + ) + + def get_search_buffer_control() -> SearchBufferControl: + "Return the UIControl to be focused when searching start." + if is_true(self.multiline): + return search_toolbar.control + else: + return search_buffer_control + + default_buffer_control = BufferControl( + buffer=default_buffer, + search_buffer_control=get_search_buffer_control, + input_processors=all_input_processors, + include_default_input_processors=False, + lexer=DynamicLexer(lambda: self.lexer), + preview_search=True, + ) + + default_buffer_window = Window( + default_buffer_control, + height=self._get_default_buffer_control_height, + get_line_prefix=partial( + self._get_line_prefix, get_prompt_text_2=get_prompt_text_2 + ), + wrap_lines=dyncond("wrap_lines"), + ) + + @Condition + def multi_column_complete_style() -> bool: + return self.complete_style == CompleteStyle.MULTI_COLUMN + + # Build the layout. + layout = HSplit( + [ + # The main input, with completion menus floating on top of it. + FloatContainer( + HSplit( + [ + ConditionalContainer( + Window( + FormattedTextControl(get_prompt_text_1), + dont_extend_height=True, + ), + Condition(has_before_fragments), + ), + ConditionalContainer( + default_buffer_window, + Condition( + lambda: get_app().layout.current_control + != search_buffer_control + ), + ), + ConditionalContainer( + Window(search_buffer_control), + Condition( + lambda: get_app().layout.current_control + == search_buffer_control + ), + ), + ] + ), + [ + # Completion menus. + # NOTE: Especially the multi-column menu needs to be + # transparent, because the shape is not always + # rectangular due to the meta-text below the menu. + Float( + xcursor=True, + ycursor=True, + transparent=True, + content=CompletionsMenu( + max_height=16, + scroll_offset=1, + extra_filter=has_focus(default_buffer) + & ~multi_column_complete_style, + ), + ), + Float( + xcursor=True, + ycursor=True, + transparent=True, + content=MultiColumnCompletionsMenu( + show_meta=True, + extra_filter=has_focus(default_buffer) + & multi_column_complete_style, + ), + ), + # The right prompt. + Float( + right=0, + bottom=0, + hide_when_covering_content=True, + content=_RPrompt(lambda: self.rprompt), + ), + ], + ), + ConditionalContainer(ValidationToolbar(), filter=~is_done), + ConditionalContainer( + system_toolbar, dyncond("enable_system_prompt") & ~is_done + ), + # In multiline mode, we use two toolbars for 'arg' and 'search'. + ConditionalContainer( + Window(FormattedTextControl(self._get_arg_text), height=1), + dyncond("multiline") & has_arg, + ), + ConditionalContainer(search_toolbar, dyncond("multiline") & ~is_done), + bottom_toolbar, + ] + ) + + return Layout(layout, default_buffer_window) + + def _create_application( + self, editing_mode: EditingMode, erase_when_done: bool + ) -> Application[_T]: + """ + Create the `Application` object. + """ + dyncond = self._dyncond + + # Default key bindings. + auto_suggest_bindings = load_auto_suggest_bindings() + open_in_editor_bindings = load_open_in_editor_bindings() + prompt_bindings = self._create_prompt_bindings() + + # Create application + application: Application[_T] = Application( + layout=self.layout, + style=DynamicStyle(lambda: self.style), + style_transformation=merge_style_transformations( + [ + DynamicStyleTransformation(lambda: self.style_transformation), + ConditionalStyleTransformation( + SwapLightAndDarkStyleTransformation(), + dyncond("swap_light_and_dark_colors"), + ), + ] + ), + include_default_pygments_style=dyncond("include_default_pygments_style"), + clipboard=DynamicClipboard(lambda: self.clipboard), + key_bindings=merge_key_bindings( + [ + merge_key_bindings( + [ + auto_suggest_bindings, + ConditionalKeyBindings( + open_in_editor_bindings, + dyncond("enable_open_in_editor") + & has_focus(DEFAULT_BUFFER), + ), + prompt_bindings, + ] + ), + DynamicKeyBindings(lambda: self.key_bindings), + ] + ), + mouse_support=dyncond("mouse_support"), + editing_mode=editing_mode, + erase_when_done=erase_when_done, + reverse_vi_search_direction=True, + color_depth=lambda: self.color_depth, cursor=DynamicCursorShapeConfig(lambda: self.cursor), - refresh_interval=self.refresh_interval, - input=self._input, - output=self._output, - ) - - # During render time, make sure that we focus the right search control - # (if we are searching). - This could be useful if people make the - # 'multiline' property dynamic. - """ - def on_render(app): - multiline = is_true(self.multiline) - current_control = app.layout.current_control - - if multiline: - if current_control == search_buffer_control: - app.layout.current_control = search_toolbar.control - app.invalidate() - else: - if current_control == search_toolbar.control: - app.layout.current_control = search_buffer_control - app.invalidate() - - app.on_render += on_render - """ - - return application - - def _create_prompt_bindings(self) -> KeyBindings: - """ - Create the KeyBindings for a prompt application. - """ - kb = KeyBindings() - handle = kb.add - default_focused = has_focus(DEFAULT_BUFFER) - - @Condition - def do_accept() -> bool: - return not is_true(self.multiline) and self.app.layout.has_focus( - DEFAULT_BUFFER - ) - - @handle("enter", filter=do_accept & default_focused) - def _accept_input(event: E) -> None: - "Accept input when enter has been pressed." - self.default_buffer.validate_and_handle() - - @Condition - def readline_complete_style() -> bool: - return self.complete_style == CompleteStyle.READLINE_LIKE - - @handle("tab", filter=readline_complete_style & default_focused) - def _complete_like_readline(event: E) -> None: - "Display completions (like Readline)." - display_completions_like_readline(event) - - @handle("c-c", filter=default_focused) + refresh_interval=self.refresh_interval, + input=self._input, + output=self._output, + ) + + # During render time, make sure that we focus the right search control + # (if we are searching). - This could be useful if people make the + # 'multiline' property dynamic. + """ + def on_render(app): + multiline = is_true(self.multiline) + current_control = app.layout.current_control + + if multiline: + if current_control == search_buffer_control: + app.layout.current_control = search_toolbar.control + app.invalidate() + else: + if current_control == search_toolbar.control: + app.layout.current_control = search_buffer_control + app.invalidate() + + app.on_render += on_render + """ + + return application + + def _create_prompt_bindings(self) -> KeyBindings: + """ + Create the KeyBindings for a prompt application. + """ + kb = KeyBindings() + handle = kb.add + default_focused = has_focus(DEFAULT_BUFFER) + + @Condition + def do_accept() -> bool: + return not is_true(self.multiline) and self.app.layout.has_focus( + DEFAULT_BUFFER + ) + + @handle("enter", filter=do_accept & default_focused) + def _accept_input(event: E) -> None: + "Accept input when enter has been pressed." + self.default_buffer.validate_and_handle() + + @Condition + def readline_complete_style() -> bool: + return self.complete_style == CompleteStyle.READLINE_LIKE + + @handle("tab", filter=readline_complete_style & default_focused) + def _complete_like_readline(event: E) -> None: + "Display completions (like Readline)." + display_completions_like_readline(event) + + @handle("c-c", filter=default_focused) @handle("<sigint>") - def _keyboard_interrupt(event: E) -> None: - "Abort when Control-C has been pressed." - event.app.exit(exception=KeyboardInterrupt, style="class:aborting") - - @Condition - def ctrl_d_condition() -> bool: - """Ctrl-D binding is only active when the default buffer is selected - and empty.""" - app = get_app() - return ( - app.current_buffer.name == DEFAULT_BUFFER - and not app.current_buffer.text - ) - - @handle("c-d", filter=ctrl_d_condition & default_focused) - def _eof(event: E) -> None: - "Exit when Control-D has been pressed." - event.app.exit(exception=EOFError, style="class:exiting") - - suspend_supported = Condition(suspend_to_background_supported) - - @Condition - def enable_suspend() -> bool: - return to_filter(self.enable_suspend)() - - @handle("c-z", filter=suspend_supported & enable_suspend) - def _suspend(event: E) -> None: - """ - Suspend process to background. - """ - event.app.suspend_to_background() - - return kb - - def prompt( - self, - # When any of these arguments are passed, this value is overwritten - # in this PromptSession. - message: Optional[AnyFormattedText] = None, - # `message` should go first, because people call it as - # positional argument. - *, - editing_mode: Optional[EditingMode] = None, - refresh_interval: Optional[float] = None, - vi_mode: Optional[bool] = None, - lexer: Optional[Lexer] = None, - completer: Optional[Completer] = None, - complete_in_thread: Optional[bool] = None, - is_password: Optional[bool] = None, - key_bindings: Optional[KeyBindingsBase] = None, - bottom_toolbar: Optional[AnyFormattedText] = None, - style: Optional[BaseStyle] = None, - color_depth: Optional[ColorDepth] = None, + def _keyboard_interrupt(event: E) -> None: + "Abort when Control-C has been pressed." + event.app.exit(exception=KeyboardInterrupt, style="class:aborting") + + @Condition + def ctrl_d_condition() -> bool: + """Ctrl-D binding is only active when the default buffer is selected + and empty.""" + app = get_app() + return ( + app.current_buffer.name == DEFAULT_BUFFER + and not app.current_buffer.text + ) + + @handle("c-d", filter=ctrl_d_condition & default_focused) + def _eof(event: E) -> None: + "Exit when Control-D has been pressed." + event.app.exit(exception=EOFError, style="class:exiting") + + suspend_supported = Condition(suspend_to_background_supported) + + @Condition + def enable_suspend() -> bool: + return to_filter(self.enable_suspend)() + + @handle("c-z", filter=suspend_supported & enable_suspend) + def _suspend(event: E) -> None: + """ + Suspend process to background. + """ + event.app.suspend_to_background() + + return kb + + def prompt( + self, + # When any of these arguments are passed, this value is overwritten + # in this PromptSession. + message: Optional[AnyFormattedText] = None, + # `message` should go first, because people call it as + # positional argument. + *, + editing_mode: Optional[EditingMode] = None, + refresh_interval: Optional[float] = None, + vi_mode: Optional[bool] = None, + lexer: Optional[Lexer] = None, + completer: Optional[Completer] = None, + complete_in_thread: Optional[bool] = None, + is_password: Optional[bool] = None, + key_bindings: Optional[KeyBindingsBase] = None, + bottom_toolbar: Optional[AnyFormattedText] = None, + style: Optional[BaseStyle] = None, + color_depth: Optional[ColorDepth] = None, cursor: Optional[AnyCursorShapeConfig] = None, - include_default_pygments_style: Optional[FilterOrBool] = None, - style_transformation: Optional[StyleTransformation] = None, - swap_light_and_dark_colors: Optional[FilterOrBool] = None, - rprompt: Optional[AnyFormattedText] = None, - multiline: Optional[FilterOrBool] = None, - prompt_continuation: Optional[PromptContinuationText] = None, - wrap_lines: Optional[FilterOrBool] = None, - enable_history_search: Optional[FilterOrBool] = None, - search_ignore_case: Optional[FilterOrBool] = None, - complete_while_typing: Optional[FilterOrBool] = None, - validate_while_typing: Optional[FilterOrBool] = None, - complete_style: Optional[CompleteStyle] = None, - auto_suggest: Optional[AutoSuggest] = None, - validator: Optional[Validator] = None, - clipboard: Optional[Clipboard] = None, - mouse_support: Optional[FilterOrBool] = None, - input_processors: Optional[List[Processor]] = None, - placeholder: Optional[AnyFormattedText] = None, - reserve_space_for_menu: Optional[int] = None, - enable_system_prompt: Optional[FilterOrBool] = None, - enable_suspend: Optional[FilterOrBool] = None, - enable_open_in_editor: Optional[FilterOrBool] = None, - tempfile_suffix: Optional[Union[str, Callable[[], str]]] = None, - tempfile: Optional[Union[str, Callable[[], str]]] = None, - # Following arguments are specific to the current `prompt()` call. - default: Union[str, Document] = "", - accept_default: bool = False, - pre_run: Optional[Callable[[], None]] = None, - set_exception_handler: bool = True, - in_thread: bool = False, - ) -> _T: - """ - Display the prompt. - - The first set of arguments is a subset of the :class:`~.PromptSession` - class itself. For these, passing in ``None`` will keep the current - values that are active in the session. Passing in a value will set the - attribute for the session, which means that it applies to the current, - but also to the next prompts. - - Note that in order to erase a ``Completer``, ``Validator`` or - ``AutoSuggest``, you can't use ``None``. Instead pass in a - ``DummyCompleter``, ``DummyValidator`` or ``DummyAutoSuggest`` instance - respectively. For a ``Lexer`` you can pass in an empty ``SimpleLexer``. - - Additional arguments, specific for this prompt: - - :param default: The default input text to be shown. (This can be edited - by the user). - :param accept_default: When `True`, automatically accept the default - value without allowing the user to edit the input. - :param pre_run: Callable, called at the start of `Application.run`. - :param in_thread: Run the prompt in a background thread; block the - current thread. This avoids interference with an event loop in the - current thread. Like `Application.run(in_thread=True)`. - - This method will raise ``KeyboardInterrupt`` when control-c has been - pressed (for abort) and ``EOFError`` when control-d has been pressed - (for exit). - """ - # NOTE: We used to create a backup of the PromptSession attributes and - # restore them after exiting the prompt. This code has been - # removed, because it was confusing and didn't really serve a use - # case. (People were changing `Application.editing_mode` - # dynamically and surprised that it was reset after every call.) - - # NOTE 2: YES, this is a lot of repeation below... - # However, it is a very convenient for a user to accept all - # these parameters in this `prompt` method as well. We could - # use `locals()` and `setattr` to avoid the repetition, but - # then we loose the advantage of mypy and pyflakes to be able - # to verify the code. - if message is not None: - self.message = message - if editing_mode is not None: - self.editing_mode = editing_mode - if refresh_interval is not None: - self.refresh_interval = refresh_interval - if vi_mode: - self.editing_mode = EditingMode.VI - if lexer is not None: - self.lexer = lexer - if completer is not None: - self.completer = completer - if complete_in_thread is not None: - self.complete_in_thread = complete_in_thread - if is_password is not None: - self.is_password = is_password - if key_bindings is not None: - self.key_bindings = key_bindings - if bottom_toolbar is not None: - self.bottom_toolbar = bottom_toolbar - if style is not None: - self.style = style - if color_depth is not None: - self.color_depth = color_depth + include_default_pygments_style: Optional[FilterOrBool] = None, + style_transformation: Optional[StyleTransformation] = None, + swap_light_and_dark_colors: Optional[FilterOrBool] = None, + rprompt: Optional[AnyFormattedText] = None, + multiline: Optional[FilterOrBool] = None, + prompt_continuation: Optional[PromptContinuationText] = None, + wrap_lines: Optional[FilterOrBool] = None, + enable_history_search: Optional[FilterOrBool] = None, + search_ignore_case: Optional[FilterOrBool] = None, + complete_while_typing: Optional[FilterOrBool] = None, + validate_while_typing: Optional[FilterOrBool] = None, + complete_style: Optional[CompleteStyle] = None, + auto_suggest: Optional[AutoSuggest] = None, + validator: Optional[Validator] = None, + clipboard: Optional[Clipboard] = None, + mouse_support: Optional[FilterOrBool] = None, + input_processors: Optional[List[Processor]] = None, + placeholder: Optional[AnyFormattedText] = None, + reserve_space_for_menu: Optional[int] = None, + enable_system_prompt: Optional[FilterOrBool] = None, + enable_suspend: Optional[FilterOrBool] = None, + enable_open_in_editor: Optional[FilterOrBool] = None, + tempfile_suffix: Optional[Union[str, Callable[[], str]]] = None, + tempfile: Optional[Union[str, Callable[[], str]]] = None, + # Following arguments are specific to the current `prompt()` call. + default: Union[str, Document] = "", + accept_default: bool = False, + pre_run: Optional[Callable[[], None]] = None, + set_exception_handler: bool = True, + in_thread: bool = False, + ) -> _T: + """ + Display the prompt. + + The first set of arguments is a subset of the :class:`~.PromptSession` + class itself. For these, passing in ``None`` will keep the current + values that are active in the session. Passing in a value will set the + attribute for the session, which means that it applies to the current, + but also to the next prompts. + + Note that in order to erase a ``Completer``, ``Validator`` or + ``AutoSuggest``, you can't use ``None``. Instead pass in a + ``DummyCompleter``, ``DummyValidator`` or ``DummyAutoSuggest`` instance + respectively. For a ``Lexer`` you can pass in an empty ``SimpleLexer``. + + Additional arguments, specific for this prompt: + + :param default: The default input text to be shown. (This can be edited + by the user). + :param accept_default: When `True`, automatically accept the default + value without allowing the user to edit the input. + :param pre_run: Callable, called at the start of `Application.run`. + :param in_thread: Run the prompt in a background thread; block the + current thread. This avoids interference with an event loop in the + current thread. Like `Application.run(in_thread=True)`. + + This method will raise ``KeyboardInterrupt`` when control-c has been + pressed (for abort) and ``EOFError`` when control-d has been pressed + (for exit). + """ + # NOTE: We used to create a backup of the PromptSession attributes and + # restore them after exiting the prompt. This code has been + # removed, because it was confusing and didn't really serve a use + # case. (People were changing `Application.editing_mode` + # dynamically and surprised that it was reset after every call.) + + # NOTE 2: YES, this is a lot of repeation below... + # However, it is a very convenient for a user to accept all + # these parameters in this `prompt` method as well. We could + # use `locals()` and `setattr` to avoid the repetition, but + # then we loose the advantage of mypy and pyflakes to be able + # to verify the code. + if message is not None: + self.message = message + if editing_mode is not None: + self.editing_mode = editing_mode + if refresh_interval is not None: + self.refresh_interval = refresh_interval + if vi_mode: + self.editing_mode = EditingMode.VI + if lexer is not None: + self.lexer = lexer + if completer is not None: + self.completer = completer + if complete_in_thread is not None: + self.complete_in_thread = complete_in_thread + if is_password is not None: + self.is_password = is_password + if key_bindings is not None: + self.key_bindings = key_bindings + if bottom_toolbar is not None: + self.bottom_toolbar = bottom_toolbar + if style is not None: + self.style = style + if color_depth is not None: + self.color_depth = color_depth if cursor is not None: self.cursor = cursor - if include_default_pygments_style is not None: - self.include_default_pygments_style = include_default_pygments_style - if style_transformation is not None: - self.style_transformation = style_transformation - if swap_light_and_dark_colors is not None: - self.swap_light_and_dark_colors = swap_light_and_dark_colors - if rprompt is not None: - self.rprompt = rprompt - if multiline is not None: - self.multiline = multiline - if prompt_continuation is not None: - self.prompt_continuation = prompt_continuation - if wrap_lines is not None: - self.wrap_lines = wrap_lines - if enable_history_search is not None: - self.enable_history_search = enable_history_search - if search_ignore_case is not None: - self.search_ignore_case = search_ignore_case - if complete_while_typing is not None: - self.complete_while_typing = complete_while_typing - if validate_while_typing is not None: - self.validate_while_typing = validate_while_typing - if complete_style is not None: - self.complete_style = complete_style - if auto_suggest is not None: - self.auto_suggest = auto_suggest - if validator is not None: - self.validator = validator - if clipboard is not None: - self.clipboard = clipboard - if mouse_support is not None: - self.mouse_support = mouse_support - if input_processors is not None: - self.input_processors = input_processors - if placeholder is not None: - self.placeholder = placeholder - if reserve_space_for_menu is not None: - self.reserve_space_for_menu = reserve_space_for_menu - if enable_system_prompt is not None: - self.enable_system_prompt = enable_system_prompt - if enable_suspend is not None: - self.enable_suspend = enable_suspend - if enable_open_in_editor is not None: - self.enable_open_in_editor = enable_open_in_editor - if tempfile_suffix is not None: - self.tempfile_suffix = tempfile_suffix - if tempfile is not None: - self.tempfile = tempfile - - self._add_pre_run_callables(pre_run, accept_default) - self.default_buffer.reset( - default if isinstance(default, Document) else Document(default) - ) - self.app.refresh_interval = self.refresh_interval # This is not reactive. - - # If we are using the default output, and have a dumb terminal. Use the - # dumb prompt. - if self._output is None and is_dumb_terminal(): - with self._dumb_prompt(self.message) as dump_app: - return dump_app.run(in_thread=in_thread) - - return self.app.run( - set_exception_handler=set_exception_handler, in_thread=in_thread - ) - - @contextmanager - def _dumb_prompt(self, message: AnyFormattedText = "") -> Iterator[Application[_T]]: - """ - Create prompt `Application` for prompt function for dumb terminals. - - Dumb terminals have minimum rendering capabilities. We can only print - text to the screen. We can't use colors, and we can't do cursor - movements. The Emacs inferior shell is an example of a dumb terminal. - - We will show the prompt, and wait for the input. We still handle arrow - keys, and all custom key bindings, but we don't really render the - cursor movements. Instead we only print the typed character that's - right before the cursor. - """ - # Send prompt to output. - self.output.write(fragment_list_to_text(to_formatted_text(self.message))) - self.output.flush() - - # Key bindings for the dumb prompt: mostly the same as the full prompt. - key_bindings: KeyBindingsBase = self._create_prompt_bindings() - if self.key_bindings: - key_bindings = merge_key_bindings([self.key_bindings, key_bindings]) - - # Create and run application. - application = cast( - Application[_T], - Application( - input=self.input, - output=DummyOutput(), - layout=self.layout, - key_bindings=key_bindings, - ), - ) - - def on_text_changed(_: object) -> None: - self.output.write(self.default_buffer.document.text_before_cursor[-1:]) - self.output.flush() - - self.default_buffer.on_text_changed += on_text_changed - - try: - yield application - finally: - # Render line ending. - self.output.write("\r\n") - self.output.flush() - - self.default_buffer.on_text_changed -= on_text_changed - - async def prompt_async( - self, - # When any of these arguments are passed, this value is overwritten - # in this PromptSession. - message: Optional[AnyFormattedText] = None, - # `message` should go first, because people call it as - # positional argument. - *, - editing_mode: Optional[EditingMode] = None, - refresh_interval: Optional[float] = None, - vi_mode: Optional[bool] = None, - lexer: Optional[Lexer] = None, - completer: Optional[Completer] = None, - complete_in_thread: Optional[bool] = None, - is_password: Optional[bool] = None, - key_bindings: Optional[KeyBindingsBase] = None, - bottom_toolbar: Optional[AnyFormattedText] = None, - style: Optional[BaseStyle] = None, - color_depth: Optional[ColorDepth] = None, + if include_default_pygments_style is not None: + self.include_default_pygments_style = include_default_pygments_style + if style_transformation is not None: + self.style_transformation = style_transformation + if swap_light_and_dark_colors is not None: + self.swap_light_and_dark_colors = swap_light_and_dark_colors + if rprompt is not None: + self.rprompt = rprompt + if multiline is not None: + self.multiline = multiline + if prompt_continuation is not None: + self.prompt_continuation = prompt_continuation + if wrap_lines is not None: + self.wrap_lines = wrap_lines + if enable_history_search is not None: + self.enable_history_search = enable_history_search + if search_ignore_case is not None: + self.search_ignore_case = search_ignore_case + if complete_while_typing is not None: + self.complete_while_typing = complete_while_typing + if validate_while_typing is not None: + self.validate_while_typing = validate_while_typing + if complete_style is not None: + self.complete_style = complete_style + if auto_suggest is not None: + self.auto_suggest = auto_suggest + if validator is not None: + self.validator = validator + if clipboard is not None: + self.clipboard = clipboard + if mouse_support is not None: + self.mouse_support = mouse_support + if input_processors is not None: + self.input_processors = input_processors + if placeholder is not None: + self.placeholder = placeholder + if reserve_space_for_menu is not None: + self.reserve_space_for_menu = reserve_space_for_menu + if enable_system_prompt is not None: + self.enable_system_prompt = enable_system_prompt + if enable_suspend is not None: + self.enable_suspend = enable_suspend + if enable_open_in_editor is not None: + self.enable_open_in_editor = enable_open_in_editor + if tempfile_suffix is not None: + self.tempfile_suffix = tempfile_suffix + if tempfile is not None: + self.tempfile = tempfile + + self._add_pre_run_callables(pre_run, accept_default) + self.default_buffer.reset( + default if isinstance(default, Document) else Document(default) + ) + self.app.refresh_interval = self.refresh_interval # This is not reactive. + + # If we are using the default output, and have a dumb terminal. Use the + # dumb prompt. + if self._output is None and is_dumb_terminal(): + with self._dumb_prompt(self.message) as dump_app: + return dump_app.run(in_thread=in_thread) + + return self.app.run( + set_exception_handler=set_exception_handler, in_thread=in_thread + ) + + @contextmanager + def _dumb_prompt(self, message: AnyFormattedText = "") -> Iterator[Application[_T]]: + """ + Create prompt `Application` for prompt function for dumb terminals. + + Dumb terminals have minimum rendering capabilities. We can only print + text to the screen. We can't use colors, and we can't do cursor + movements. The Emacs inferior shell is an example of a dumb terminal. + + We will show the prompt, and wait for the input. We still handle arrow + keys, and all custom key bindings, but we don't really render the + cursor movements. Instead we only print the typed character that's + right before the cursor. + """ + # Send prompt to output. + self.output.write(fragment_list_to_text(to_formatted_text(self.message))) + self.output.flush() + + # Key bindings for the dumb prompt: mostly the same as the full prompt. + key_bindings: KeyBindingsBase = self._create_prompt_bindings() + if self.key_bindings: + key_bindings = merge_key_bindings([self.key_bindings, key_bindings]) + + # Create and run application. + application = cast( + Application[_T], + Application( + input=self.input, + output=DummyOutput(), + layout=self.layout, + key_bindings=key_bindings, + ), + ) + + def on_text_changed(_: object) -> None: + self.output.write(self.default_buffer.document.text_before_cursor[-1:]) + self.output.flush() + + self.default_buffer.on_text_changed += on_text_changed + + try: + yield application + finally: + # Render line ending. + self.output.write("\r\n") + self.output.flush() + + self.default_buffer.on_text_changed -= on_text_changed + + async def prompt_async( + self, + # When any of these arguments are passed, this value is overwritten + # in this PromptSession. + message: Optional[AnyFormattedText] = None, + # `message` should go first, because people call it as + # positional argument. + *, + editing_mode: Optional[EditingMode] = None, + refresh_interval: Optional[float] = None, + vi_mode: Optional[bool] = None, + lexer: Optional[Lexer] = None, + completer: Optional[Completer] = None, + complete_in_thread: Optional[bool] = None, + is_password: Optional[bool] = None, + key_bindings: Optional[KeyBindingsBase] = None, + bottom_toolbar: Optional[AnyFormattedText] = None, + style: Optional[BaseStyle] = None, + color_depth: Optional[ColorDepth] = None, cursor: Optional[CursorShapeConfig] = None, - include_default_pygments_style: Optional[FilterOrBool] = None, - style_transformation: Optional[StyleTransformation] = None, - swap_light_and_dark_colors: Optional[FilterOrBool] = None, - rprompt: Optional[AnyFormattedText] = None, - multiline: Optional[FilterOrBool] = None, - prompt_continuation: Optional[PromptContinuationText] = None, - wrap_lines: Optional[FilterOrBool] = None, - enable_history_search: Optional[FilterOrBool] = None, - search_ignore_case: Optional[FilterOrBool] = None, - complete_while_typing: Optional[FilterOrBool] = None, - validate_while_typing: Optional[FilterOrBool] = None, - complete_style: Optional[CompleteStyle] = None, - auto_suggest: Optional[AutoSuggest] = None, - validator: Optional[Validator] = None, - clipboard: Optional[Clipboard] = None, - mouse_support: Optional[FilterOrBool] = None, - input_processors: Optional[List[Processor]] = None, - placeholder: Optional[AnyFormattedText] = None, - reserve_space_for_menu: Optional[int] = None, - enable_system_prompt: Optional[FilterOrBool] = None, - enable_suspend: Optional[FilterOrBool] = None, - enable_open_in_editor: Optional[FilterOrBool] = None, - tempfile_suffix: Optional[Union[str, Callable[[], str]]] = None, - tempfile: Optional[Union[str, Callable[[], str]]] = None, - # Following arguments are specific to the current `prompt()` call. - default: Union[str, Document] = "", - accept_default: bool = False, - pre_run: Optional[Callable[[], None]] = None, - set_exception_handler: bool = True, - ) -> _T: - - if message is not None: - self.message = message - if editing_mode is not None: - self.editing_mode = editing_mode - if refresh_interval is not None: - self.refresh_interval = refresh_interval - if vi_mode: - self.editing_mode = EditingMode.VI - if lexer is not None: - self.lexer = lexer - if completer is not None: - self.completer = completer - if complete_in_thread is not None: - self.complete_in_thread = complete_in_thread - if is_password is not None: - self.is_password = is_password - if key_bindings is not None: - self.key_bindings = key_bindings - if bottom_toolbar is not None: - self.bottom_toolbar = bottom_toolbar - if style is not None: - self.style = style - if color_depth is not None: - self.color_depth = color_depth + include_default_pygments_style: Optional[FilterOrBool] = None, + style_transformation: Optional[StyleTransformation] = None, + swap_light_and_dark_colors: Optional[FilterOrBool] = None, + rprompt: Optional[AnyFormattedText] = None, + multiline: Optional[FilterOrBool] = None, + prompt_continuation: Optional[PromptContinuationText] = None, + wrap_lines: Optional[FilterOrBool] = None, + enable_history_search: Optional[FilterOrBool] = None, + search_ignore_case: Optional[FilterOrBool] = None, + complete_while_typing: Optional[FilterOrBool] = None, + validate_while_typing: Optional[FilterOrBool] = None, + complete_style: Optional[CompleteStyle] = None, + auto_suggest: Optional[AutoSuggest] = None, + validator: Optional[Validator] = None, + clipboard: Optional[Clipboard] = None, + mouse_support: Optional[FilterOrBool] = None, + input_processors: Optional[List[Processor]] = None, + placeholder: Optional[AnyFormattedText] = None, + reserve_space_for_menu: Optional[int] = None, + enable_system_prompt: Optional[FilterOrBool] = None, + enable_suspend: Optional[FilterOrBool] = None, + enable_open_in_editor: Optional[FilterOrBool] = None, + tempfile_suffix: Optional[Union[str, Callable[[], str]]] = None, + tempfile: Optional[Union[str, Callable[[], str]]] = None, + # Following arguments are specific to the current `prompt()` call. + default: Union[str, Document] = "", + accept_default: bool = False, + pre_run: Optional[Callable[[], None]] = None, + set_exception_handler: bool = True, + ) -> _T: + + if message is not None: + self.message = message + if editing_mode is not None: + self.editing_mode = editing_mode + if refresh_interval is not None: + self.refresh_interval = refresh_interval + if vi_mode: + self.editing_mode = EditingMode.VI + if lexer is not None: + self.lexer = lexer + if completer is not None: + self.completer = completer + if complete_in_thread is not None: + self.complete_in_thread = complete_in_thread + if is_password is not None: + self.is_password = is_password + if key_bindings is not None: + self.key_bindings = key_bindings + if bottom_toolbar is not None: + self.bottom_toolbar = bottom_toolbar + if style is not None: + self.style = style + if color_depth is not None: + self.color_depth = color_depth if cursor is not None: self.cursor = cursor - if include_default_pygments_style is not None: - self.include_default_pygments_style = include_default_pygments_style - if style_transformation is not None: - self.style_transformation = style_transformation - if swap_light_and_dark_colors is not None: - self.swap_light_and_dark_colors = swap_light_and_dark_colors - if rprompt is not None: - self.rprompt = rprompt - if multiline is not None: - self.multiline = multiline - if prompt_continuation is not None: - self.prompt_continuation = prompt_continuation - if wrap_lines is not None: - self.wrap_lines = wrap_lines - if enable_history_search is not None: - self.enable_history_search = enable_history_search - if search_ignore_case is not None: - self.search_ignore_case = search_ignore_case - if complete_while_typing is not None: - self.complete_while_typing = complete_while_typing - if validate_while_typing is not None: - self.validate_while_typing = validate_while_typing - if complete_style is not None: - self.complete_style = complete_style - if auto_suggest is not None: - self.auto_suggest = auto_suggest - if validator is not None: - self.validator = validator - if clipboard is not None: - self.clipboard = clipboard - if mouse_support is not None: - self.mouse_support = mouse_support - if input_processors is not None: - self.input_processors = input_processors - if placeholder is not None: - self.placeholder = placeholder - if reserve_space_for_menu is not None: - self.reserve_space_for_menu = reserve_space_for_menu - if enable_system_prompt is not None: - self.enable_system_prompt = enable_system_prompt - if enable_suspend is not None: - self.enable_suspend = enable_suspend - if enable_open_in_editor is not None: - self.enable_open_in_editor = enable_open_in_editor - if tempfile_suffix is not None: - self.tempfile_suffix = tempfile_suffix - if tempfile is not None: - self.tempfile = tempfile - - self._add_pre_run_callables(pre_run, accept_default) - self.default_buffer.reset( - default if isinstance(default, Document) else Document(default) - ) - self.app.refresh_interval = self.refresh_interval # This is not reactive. - - # If we are using the default output, and have a dumb terminal. Use the - # dumb prompt. - if self._output is None and is_dumb_terminal(): - with self._dumb_prompt(self.message) as dump_app: - return await dump_app.run_async() - - return await self.app.run_async(set_exception_handler=set_exception_handler) - - def _add_pre_run_callables( - self, pre_run: Optional[Callable[[], None]], accept_default: bool - ) -> None: - def pre_run2() -> None: - if pre_run: - pre_run() - - if accept_default: - # Validate and handle input. We use `call_from_executor` in - # order to run it "soon" (during the next iteration of the - # event loop), instead of right now. Otherwise, it won't - # display the default value. - get_event_loop().call_soon(self.default_buffer.validate_and_handle) - - self.app.pre_run_callables.append(pre_run2) - - @property - def editing_mode(self) -> EditingMode: - return self.app.editing_mode - - @editing_mode.setter - def editing_mode(self, value: EditingMode) -> None: - self.app.editing_mode = value - - def _get_default_buffer_control_height(self) -> Dimension: - # If there is an autocompletion menu to be shown, make sure that our - # layout has at least a minimal height in order to display it. - if ( - self.completer is not None - and self.complete_style != CompleteStyle.READLINE_LIKE - ): - space = self.reserve_space_for_menu - else: - space = 0 - - if space and not get_app().is_done: - buff = self.default_buffer - - # Reserve the space, either when there are completions, or when - # `complete_while_typing` is true and we expect completions very - # soon. - if buff.complete_while_typing() or buff.complete_state is not None: - return Dimension(min=space) - - return Dimension() - - def _get_prompt(self) -> StyleAndTextTuples: - return to_formatted_text(self.message, style="class:prompt") - - def _get_continuation( - self, width: int, line_number: int, wrap_count: int - ) -> StyleAndTextTuples: - """ - Insert the prompt continuation. - - :param width: The width that was used for the prompt. (more or less can - be used.) - :param line_number: - :param wrap_count: Amount of times that the line has been wrapped. - """ - prompt_continuation = self.prompt_continuation - - if callable(prompt_continuation): - continuation: AnyFormattedText = prompt_continuation( - width, line_number, wrap_count - ) - else: - continuation = prompt_continuation - - # When the continuation prompt is not given, choose the same width as - # the actual prompt. - if continuation is None and is_true(self.multiline): - continuation = " " * width - - return to_formatted_text(continuation, style="class:prompt-continuation") - - def _get_line_prefix( - self, - line_number: int, - wrap_count: int, - get_prompt_text_2: _StyleAndTextTuplesCallable, - ) -> StyleAndTextTuples: - """ - Return whatever needs to be inserted before every line. - (the prompt, or a line continuation.) - """ - # First line: display the "arg" or the prompt. - if line_number == 0 and wrap_count == 0: - if not is_true(self.multiline) and get_app().key_processor.arg is not None: - return self._inline_arg() - else: - return get_prompt_text_2() - - # For the next lines, display the appropriate continuation. - prompt_width = get_cwidth(fragment_list_to_text(get_prompt_text_2())) - return self._get_continuation(prompt_width, line_number, wrap_count) - - def _get_arg_text(self) -> StyleAndTextTuples: - "'arg' toolbar, for in multiline mode." - arg = self.app.key_processor.arg - if arg is None: - # Should not happen because of the `has_arg` filter in the layout. - return [] - - if arg == "-": - arg = "-1" - - return [("class:arg-toolbar", "Repeat: "), ("class:arg-toolbar.text", arg)] - - def _inline_arg(self) -> StyleAndTextTuples: - "'arg' prefix, for in single line mode." - app = get_app() - if app.key_processor.arg is None: - return [] - else: - arg = app.key_processor.arg - - return [ - ("class:prompt.arg", "(arg: "), - ("class:prompt.arg.text", str(arg)), - ("class:prompt.arg", ") "), - ] - - # Expose the Input and Output objects as attributes, mainly for - # backward-compatibility. - - @property - def input(self) -> Input: - return self.app.input - - @property - def output(self) -> Output: - return self.app.output - - -def prompt( - message: Optional[AnyFormattedText] = None, - *, - history: Optional[History] = None, - editing_mode: Optional[EditingMode] = None, - refresh_interval: Optional[float] = None, - vi_mode: Optional[bool] = None, - lexer: Optional[Lexer] = None, - completer: Optional[Completer] = None, - complete_in_thread: Optional[bool] = None, - is_password: Optional[bool] = None, - key_bindings: Optional[KeyBindingsBase] = None, - bottom_toolbar: Optional[AnyFormattedText] = None, - style: Optional[BaseStyle] = None, - color_depth: Optional[ColorDepth] = None, + if include_default_pygments_style is not None: + self.include_default_pygments_style = include_default_pygments_style + if style_transformation is not None: + self.style_transformation = style_transformation + if swap_light_and_dark_colors is not None: + self.swap_light_and_dark_colors = swap_light_and_dark_colors + if rprompt is not None: + self.rprompt = rprompt + if multiline is not None: + self.multiline = multiline + if prompt_continuation is not None: + self.prompt_continuation = prompt_continuation + if wrap_lines is not None: + self.wrap_lines = wrap_lines + if enable_history_search is not None: + self.enable_history_search = enable_history_search + if search_ignore_case is not None: + self.search_ignore_case = search_ignore_case + if complete_while_typing is not None: + self.complete_while_typing = complete_while_typing + if validate_while_typing is not None: + self.validate_while_typing = validate_while_typing + if complete_style is not None: + self.complete_style = complete_style + if auto_suggest is not None: + self.auto_suggest = auto_suggest + if validator is not None: + self.validator = validator + if clipboard is not None: + self.clipboard = clipboard + if mouse_support is not None: + self.mouse_support = mouse_support + if input_processors is not None: + self.input_processors = input_processors + if placeholder is not None: + self.placeholder = placeholder + if reserve_space_for_menu is not None: + self.reserve_space_for_menu = reserve_space_for_menu + if enable_system_prompt is not None: + self.enable_system_prompt = enable_system_prompt + if enable_suspend is not None: + self.enable_suspend = enable_suspend + if enable_open_in_editor is not None: + self.enable_open_in_editor = enable_open_in_editor + if tempfile_suffix is not None: + self.tempfile_suffix = tempfile_suffix + if tempfile is not None: + self.tempfile = tempfile + + self._add_pre_run_callables(pre_run, accept_default) + self.default_buffer.reset( + default if isinstance(default, Document) else Document(default) + ) + self.app.refresh_interval = self.refresh_interval # This is not reactive. + + # If we are using the default output, and have a dumb terminal. Use the + # dumb prompt. + if self._output is None and is_dumb_terminal(): + with self._dumb_prompt(self.message) as dump_app: + return await dump_app.run_async() + + return await self.app.run_async(set_exception_handler=set_exception_handler) + + def _add_pre_run_callables( + self, pre_run: Optional[Callable[[], None]], accept_default: bool + ) -> None: + def pre_run2() -> None: + if pre_run: + pre_run() + + if accept_default: + # Validate and handle input. We use `call_from_executor` in + # order to run it "soon" (during the next iteration of the + # event loop), instead of right now. Otherwise, it won't + # display the default value. + get_event_loop().call_soon(self.default_buffer.validate_and_handle) + + self.app.pre_run_callables.append(pre_run2) + + @property + def editing_mode(self) -> EditingMode: + return self.app.editing_mode + + @editing_mode.setter + def editing_mode(self, value: EditingMode) -> None: + self.app.editing_mode = value + + def _get_default_buffer_control_height(self) -> Dimension: + # If there is an autocompletion menu to be shown, make sure that our + # layout has at least a minimal height in order to display it. + if ( + self.completer is not None + and self.complete_style != CompleteStyle.READLINE_LIKE + ): + space = self.reserve_space_for_menu + else: + space = 0 + + if space and not get_app().is_done: + buff = self.default_buffer + + # Reserve the space, either when there are completions, or when + # `complete_while_typing` is true and we expect completions very + # soon. + if buff.complete_while_typing() or buff.complete_state is not None: + return Dimension(min=space) + + return Dimension() + + def _get_prompt(self) -> StyleAndTextTuples: + return to_formatted_text(self.message, style="class:prompt") + + def _get_continuation( + self, width: int, line_number: int, wrap_count: int + ) -> StyleAndTextTuples: + """ + Insert the prompt continuation. + + :param width: The width that was used for the prompt. (more or less can + be used.) + :param line_number: + :param wrap_count: Amount of times that the line has been wrapped. + """ + prompt_continuation = self.prompt_continuation + + if callable(prompt_continuation): + continuation: AnyFormattedText = prompt_continuation( + width, line_number, wrap_count + ) + else: + continuation = prompt_continuation + + # When the continuation prompt is not given, choose the same width as + # the actual prompt. + if continuation is None and is_true(self.multiline): + continuation = " " * width + + return to_formatted_text(continuation, style="class:prompt-continuation") + + def _get_line_prefix( + self, + line_number: int, + wrap_count: int, + get_prompt_text_2: _StyleAndTextTuplesCallable, + ) -> StyleAndTextTuples: + """ + Return whatever needs to be inserted before every line. + (the prompt, or a line continuation.) + """ + # First line: display the "arg" or the prompt. + if line_number == 0 and wrap_count == 0: + if not is_true(self.multiline) and get_app().key_processor.arg is not None: + return self._inline_arg() + else: + return get_prompt_text_2() + + # For the next lines, display the appropriate continuation. + prompt_width = get_cwidth(fragment_list_to_text(get_prompt_text_2())) + return self._get_continuation(prompt_width, line_number, wrap_count) + + def _get_arg_text(self) -> StyleAndTextTuples: + "'arg' toolbar, for in multiline mode." + arg = self.app.key_processor.arg + if arg is None: + # Should not happen because of the `has_arg` filter in the layout. + return [] + + if arg == "-": + arg = "-1" + + return [("class:arg-toolbar", "Repeat: "), ("class:arg-toolbar.text", arg)] + + def _inline_arg(self) -> StyleAndTextTuples: + "'arg' prefix, for in single line mode." + app = get_app() + if app.key_processor.arg is None: + return [] + else: + arg = app.key_processor.arg + + return [ + ("class:prompt.arg", "(arg: "), + ("class:prompt.arg.text", str(arg)), + ("class:prompt.arg", ") "), + ] + + # Expose the Input and Output objects as attributes, mainly for + # backward-compatibility. + + @property + def input(self) -> Input: + return self.app.input + + @property + def output(self) -> Output: + return self.app.output + + +def prompt( + message: Optional[AnyFormattedText] = None, + *, + history: Optional[History] = None, + editing_mode: Optional[EditingMode] = None, + refresh_interval: Optional[float] = None, + vi_mode: Optional[bool] = None, + lexer: Optional[Lexer] = None, + completer: Optional[Completer] = None, + complete_in_thread: Optional[bool] = None, + is_password: Optional[bool] = None, + key_bindings: Optional[KeyBindingsBase] = None, + bottom_toolbar: Optional[AnyFormattedText] = None, + style: Optional[BaseStyle] = None, + color_depth: Optional[ColorDepth] = None, cursor: AnyCursorShapeConfig = None, - include_default_pygments_style: Optional[FilterOrBool] = None, - style_transformation: Optional[StyleTransformation] = None, - swap_light_and_dark_colors: Optional[FilterOrBool] = None, - rprompt: Optional[AnyFormattedText] = None, - multiline: Optional[FilterOrBool] = None, - prompt_continuation: Optional[PromptContinuationText] = None, - wrap_lines: Optional[FilterOrBool] = None, - enable_history_search: Optional[FilterOrBool] = None, - search_ignore_case: Optional[FilterOrBool] = None, - complete_while_typing: Optional[FilterOrBool] = None, - validate_while_typing: Optional[FilterOrBool] = None, - complete_style: Optional[CompleteStyle] = None, - auto_suggest: Optional[AutoSuggest] = None, - validator: Optional[Validator] = None, - clipboard: Optional[Clipboard] = None, - mouse_support: Optional[FilterOrBool] = None, - input_processors: Optional[List[Processor]] = None, - placeholder: Optional[AnyFormattedText] = None, - reserve_space_for_menu: Optional[int] = None, - enable_system_prompt: Optional[FilterOrBool] = None, - enable_suspend: Optional[FilterOrBool] = None, - enable_open_in_editor: Optional[FilterOrBool] = None, - tempfile_suffix: Optional[Union[str, Callable[[], str]]] = None, - tempfile: Optional[Union[str, Callable[[], str]]] = None, - # Following arguments are specific to the current `prompt()` call. - default: str = "", - accept_default: bool = False, - pre_run: Optional[Callable[[], None]] = None, -) -> str: - """ - The global `prompt` function. This will create a new `PromptSession` - instance for every call. - """ - # The history is the only attribute that has to be passed to the - # `PromptSession`, it can't be passed into the `prompt()` method. - session: PromptSession[str] = PromptSession(history=history) - - return session.prompt( - message, - editing_mode=editing_mode, - refresh_interval=refresh_interval, - vi_mode=vi_mode, - lexer=lexer, - completer=completer, - complete_in_thread=complete_in_thread, - is_password=is_password, - key_bindings=key_bindings, - bottom_toolbar=bottom_toolbar, - style=style, - color_depth=color_depth, + include_default_pygments_style: Optional[FilterOrBool] = None, + style_transformation: Optional[StyleTransformation] = None, + swap_light_and_dark_colors: Optional[FilterOrBool] = None, + rprompt: Optional[AnyFormattedText] = None, + multiline: Optional[FilterOrBool] = None, + prompt_continuation: Optional[PromptContinuationText] = None, + wrap_lines: Optional[FilterOrBool] = None, + enable_history_search: Optional[FilterOrBool] = None, + search_ignore_case: Optional[FilterOrBool] = None, + complete_while_typing: Optional[FilterOrBool] = None, + validate_while_typing: Optional[FilterOrBool] = None, + complete_style: Optional[CompleteStyle] = None, + auto_suggest: Optional[AutoSuggest] = None, + validator: Optional[Validator] = None, + clipboard: Optional[Clipboard] = None, + mouse_support: Optional[FilterOrBool] = None, + input_processors: Optional[List[Processor]] = None, + placeholder: Optional[AnyFormattedText] = None, + reserve_space_for_menu: Optional[int] = None, + enable_system_prompt: Optional[FilterOrBool] = None, + enable_suspend: Optional[FilterOrBool] = None, + enable_open_in_editor: Optional[FilterOrBool] = None, + tempfile_suffix: Optional[Union[str, Callable[[], str]]] = None, + tempfile: Optional[Union[str, Callable[[], str]]] = None, + # Following arguments are specific to the current `prompt()` call. + default: str = "", + accept_default: bool = False, + pre_run: Optional[Callable[[], None]] = None, +) -> str: + """ + The global `prompt` function. This will create a new `PromptSession` + instance for every call. + """ + # The history is the only attribute that has to be passed to the + # `PromptSession`, it can't be passed into the `prompt()` method. + session: PromptSession[str] = PromptSession(history=history) + + return session.prompt( + message, + editing_mode=editing_mode, + refresh_interval=refresh_interval, + vi_mode=vi_mode, + lexer=lexer, + completer=completer, + complete_in_thread=complete_in_thread, + is_password=is_password, + key_bindings=key_bindings, + bottom_toolbar=bottom_toolbar, + style=style, + color_depth=color_depth, cursor=cursor, - include_default_pygments_style=include_default_pygments_style, - style_transformation=style_transformation, - swap_light_and_dark_colors=swap_light_and_dark_colors, - rprompt=rprompt, - multiline=multiline, - prompt_continuation=prompt_continuation, - wrap_lines=wrap_lines, - enable_history_search=enable_history_search, - search_ignore_case=search_ignore_case, - complete_while_typing=complete_while_typing, - validate_while_typing=validate_while_typing, - complete_style=complete_style, - auto_suggest=auto_suggest, - validator=validator, - clipboard=clipboard, - mouse_support=mouse_support, - input_processors=input_processors, - placeholder=placeholder, - reserve_space_for_menu=reserve_space_for_menu, - enable_system_prompt=enable_system_prompt, - enable_suspend=enable_suspend, - enable_open_in_editor=enable_open_in_editor, - tempfile_suffix=tempfile_suffix, - tempfile=tempfile, - default=default, - accept_default=accept_default, - pre_run=pre_run, - ) - - -prompt.__doc__ = PromptSession.prompt.__doc__ - - -def create_confirm_session( - message: str, suffix: str = " (y/n) " -) -> PromptSession[bool]: - """ - Create a `PromptSession` object for the 'confirm' function. - """ - bindings = KeyBindings() - - @bindings.add("y") - @bindings.add("Y") - def yes(event: E) -> None: - session.default_buffer.text = "y" - event.app.exit(result=True) - - @bindings.add("n") - @bindings.add("N") - def no(event: E) -> None: - session.default_buffer.text = "n" - event.app.exit(result=False) - - @bindings.add(Keys.Any) - def _(event: E) -> None: - "Disallow inserting other text." - pass - - complete_message = merge_formatted_text([message, suffix]) - session: PromptSession[bool] = PromptSession( - complete_message, key_bindings=bindings - ) - return session - - -def confirm(message: str = "Confirm?", suffix: str = " (y/n) ") -> bool: - """ - Display a confirmation prompt that returns True/False. - """ - session = create_confirm_session(message, suffix) - return session.prompt() + include_default_pygments_style=include_default_pygments_style, + style_transformation=style_transformation, + swap_light_and_dark_colors=swap_light_and_dark_colors, + rprompt=rprompt, + multiline=multiline, + prompt_continuation=prompt_continuation, + wrap_lines=wrap_lines, + enable_history_search=enable_history_search, + search_ignore_case=search_ignore_case, + complete_while_typing=complete_while_typing, + validate_while_typing=validate_while_typing, + complete_style=complete_style, + auto_suggest=auto_suggest, + validator=validator, + clipboard=clipboard, + mouse_support=mouse_support, + input_processors=input_processors, + placeholder=placeholder, + reserve_space_for_menu=reserve_space_for_menu, + enable_system_prompt=enable_system_prompt, + enable_suspend=enable_suspend, + enable_open_in_editor=enable_open_in_editor, + tempfile_suffix=tempfile_suffix, + tempfile=tempfile, + default=default, + accept_default=accept_default, + pre_run=pre_run, + ) + + +prompt.__doc__ = PromptSession.prompt.__doc__ + + +def create_confirm_session( + message: str, suffix: str = " (y/n) " +) -> PromptSession[bool]: + """ + Create a `PromptSession` object for the 'confirm' function. + """ + bindings = KeyBindings() + + @bindings.add("y") + @bindings.add("Y") + def yes(event: E) -> None: + session.default_buffer.text = "y" + event.app.exit(result=True) + + @bindings.add("n") + @bindings.add("N") + def no(event: E) -> None: + session.default_buffer.text = "n" + event.app.exit(result=False) + + @bindings.add(Keys.Any) + def _(event: E) -> None: + "Disallow inserting other text." + pass + + complete_message = merge_formatted_text([message, suffix]) + session: PromptSession[bool] = PromptSession( + complete_message, key_bindings=bindings + ) + return session + + +def confirm(message: str = "Confirm?", suffix: str = " (y/n) ") -> bool: + """ + Display a confirmation prompt that returns True/False. + """ + session = create_confirm_session(message, suffix) + return session.prompt() diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/utils.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/utils.py index acb4ae0e7e..4e2a532b34 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/utils.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/shortcuts/utils.py @@ -1,239 +1,239 @@ -from asyncio.events import AbstractEventLoop -from typing import TYPE_CHECKING, Any, Optional, TextIO - -from prompt_toolkit.application import Application -from prompt_toolkit.application.current import get_app_or_none, get_app_session -from prompt_toolkit.application.run_in_terminal import run_in_terminal -from prompt_toolkit.eventloop import get_event_loop -from prompt_toolkit.formatted_text import ( - FormattedText, - StyleAndTextTuples, - to_formatted_text, -) -from prompt_toolkit.input import DummyInput -from prompt_toolkit.layout import Layout -from prompt_toolkit.output import ColorDepth, Output -from prompt_toolkit.output.defaults import create_output -from prompt_toolkit.renderer import ( - print_formatted_text as renderer_print_formatted_text, -) -from prompt_toolkit.styles import ( - BaseStyle, - StyleTransformation, - default_pygments_style, - default_ui_style, - merge_styles, -) - -if TYPE_CHECKING: - from prompt_toolkit.layout.containers import AnyContainer - -__all__ = [ - "print_formatted_text", - "print_container", - "clear", - "set_title", - "clear_title", -] - - -def print_formatted_text( - *values: Any, - sep: str = " ", - end: str = "\n", - file: Optional[TextIO] = None, - flush: bool = False, - style: Optional[BaseStyle] = None, - output: Optional[Output] = None, - color_depth: Optional[ColorDepth] = None, - style_transformation: Optional[StyleTransformation] = None, - include_default_pygments_style: bool = True, -) -> None: - """ - :: - - print_formatted_text(*values, sep=' ', end='\\n', file=None, flush=False, style=None, output=None) - - Print text to stdout. This is supposed to be compatible with Python's print - function, but supports printing of formatted text. You can pass a - :class:`~prompt_toolkit.formatted_text.FormattedText`, - :class:`~prompt_toolkit.formatted_text.HTML` or - :class:`~prompt_toolkit.formatted_text.ANSI` object to print formatted - text. - - * Print HTML as follows:: - - print_formatted_text(HTML('<i>Some italic text</i> <ansired>This is red!</ansired>')) - - style = Style.from_dict({ - 'hello': '#ff0066', - 'world': '#884444 italic', - }) - print_formatted_text(HTML('<hello>Hello</hello> <world>world</world>!'), style=style) - - * Print a list of (style_str, text) tuples in the given style to the - output. E.g.:: - - style = Style.from_dict({ - 'hello': '#ff0066', - 'world': '#884444 italic', - }) - fragments = FormattedText([ - ('class:hello', 'Hello'), - ('class:world', 'World'), - ]) - print_formatted_text(fragments, style=style) - - If you want to print a list of Pygments tokens, wrap it in - :class:`~prompt_toolkit.formatted_text.PygmentsTokens` to do the - conversion. - - If a prompt_toolkit `Application` is currently running, this will always - print above the application or prompt (similar to `patch_stdout`). So, - `print_formatted_text` will erase the current application, print the text, - and render the application again. - - :param values: Any kind of printable object, or formatted string. - :param sep: String inserted between values, default a space. - :param end: String appended after the last value, default a newline. - :param style: :class:`.Style` instance for the color scheme. - :param include_default_pygments_style: `bool`. Include the default Pygments - style when set to `True` (the default). - """ - assert not (output and file) - - # Create Output object. - if output is None: - if file: - output = create_output(stdout=file) - else: - output = get_app_session().output - - assert isinstance(output, Output) - - # Get color depth. - color_depth = color_depth or output.get_default_color_depth() - - # Merges values. - def to_text(val: Any) -> StyleAndTextTuples: - # Normal lists which are not instances of `FormattedText` are - # considered plain text. - if isinstance(val, list) and not isinstance(val, FormattedText): - return to_formatted_text("{0}".format(val)) - return to_formatted_text(val, auto_convert=True) - - fragments = [] - for i, value in enumerate(values): - fragments.extend(to_text(value)) - - if sep and i != len(values) - 1: - fragments.extend(to_text(sep)) - - fragments.extend(to_text(end)) - - # Print output. - def render() -> None: - assert isinstance(output, Output) - - renderer_print_formatted_text( - output, - fragments, - _create_merged_style( - style, include_default_pygments_style=include_default_pygments_style - ), - color_depth=color_depth, - style_transformation=style_transformation, - ) - - # Flush the output stream. - if flush: - output.flush() - - # If an application is running, print above the app. This does not require - # `patch_stdout`. - loop: Optional[AbstractEventLoop] = None - - app = get_app_or_none() - if app is not None: - loop = app.loop - - if loop is not None: - loop.call_soon_threadsafe(lambda: run_in_terminal(render)) - else: - render() - - -def print_container( - container: "AnyContainer", - file: Optional[TextIO] = None, - style: Optional[BaseStyle] = None, - include_default_pygments_style: bool = True, -) -> None: - """ - Print any layout to the output in a non-interactive way. - - Example usage:: - - from prompt_toolkit.widgets import Frame, TextArea - print_container( - Frame(TextArea(text='Hello world!'))) - """ - if file: - output = create_output(stdout=file) - else: - output = get_app_session().output - - def exit_immediately() -> None: - # Use `call_from_executor` to exit "soon", so that we still render one - # initial time, before exiting the application. - get_event_loop().call_soon(lambda: app.exit()) - - app: Application[None] = Application( - layout=Layout(container=container), - output=output, - input=DummyInput(), - style=_create_merged_style( - style, include_default_pygments_style=include_default_pygments_style - ), - ) - app.run(pre_run=exit_immediately, in_thread=True) - - -def _create_merged_style( - style: Optional[BaseStyle], include_default_pygments_style: bool -) -> BaseStyle: - """ - Merge user defined style with built-in style. - """ - styles = [default_ui_style()] - if include_default_pygments_style: - styles.append(default_pygments_style()) - if style: - styles.append(style) - - return merge_styles(styles) - - -def clear() -> None: - """ - Clear the screen. - """ - output = get_app_session().output - output.erase_screen() - output.cursor_goto(0, 0) - output.flush() - - -def set_title(text: str) -> None: - """ - Set the terminal title. - """ - output = get_app_session().output - output.set_title(text) - - -def clear_title() -> None: - """ - Erase the current title. - """ - set_title("") +from asyncio.events import AbstractEventLoop +from typing import TYPE_CHECKING, Any, Optional, TextIO + +from prompt_toolkit.application import Application +from prompt_toolkit.application.current import get_app_or_none, get_app_session +from prompt_toolkit.application.run_in_terminal import run_in_terminal +from prompt_toolkit.eventloop import get_event_loop +from prompt_toolkit.formatted_text import ( + FormattedText, + StyleAndTextTuples, + to_formatted_text, +) +from prompt_toolkit.input import DummyInput +from prompt_toolkit.layout import Layout +from prompt_toolkit.output import ColorDepth, Output +from prompt_toolkit.output.defaults import create_output +from prompt_toolkit.renderer import ( + print_formatted_text as renderer_print_formatted_text, +) +from prompt_toolkit.styles import ( + BaseStyle, + StyleTransformation, + default_pygments_style, + default_ui_style, + merge_styles, +) + +if TYPE_CHECKING: + from prompt_toolkit.layout.containers import AnyContainer + +__all__ = [ + "print_formatted_text", + "print_container", + "clear", + "set_title", + "clear_title", +] + + +def print_formatted_text( + *values: Any, + sep: str = " ", + end: str = "\n", + file: Optional[TextIO] = None, + flush: bool = False, + style: Optional[BaseStyle] = None, + output: Optional[Output] = None, + color_depth: Optional[ColorDepth] = None, + style_transformation: Optional[StyleTransformation] = None, + include_default_pygments_style: bool = True, +) -> None: + """ + :: + + print_formatted_text(*values, sep=' ', end='\\n', file=None, flush=False, style=None, output=None) + + Print text to stdout. This is supposed to be compatible with Python's print + function, but supports printing of formatted text. You can pass a + :class:`~prompt_toolkit.formatted_text.FormattedText`, + :class:`~prompt_toolkit.formatted_text.HTML` or + :class:`~prompt_toolkit.formatted_text.ANSI` object to print formatted + text. + + * Print HTML as follows:: + + print_formatted_text(HTML('<i>Some italic text</i> <ansired>This is red!</ansired>')) + + style = Style.from_dict({ + 'hello': '#ff0066', + 'world': '#884444 italic', + }) + print_formatted_text(HTML('<hello>Hello</hello> <world>world</world>!'), style=style) + + * Print a list of (style_str, text) tuples in the given style to the + output. E.g.:: + + style = Style.from_dict({ + 'hello': '#ff0066', + 'world': '#884444 italic', + }) + fragments = FormattedText([ + ('class:hello', 'Hello'), + ('class:world', 'World'), + ]) + print_formatted_text(fragments, style=style) + + If you want to print a list of Pygments tokens, wrap it in + :class:`~prompt_toolkit.formatted_text.PygmentsTokens` to do the + conversion. + + If a prompt_toolkit `Application` is currently running, this will always + print above the application or prompt (similar to `patch_stdout`). So, + `print_formatted_text` will erase the current application, print the text, + and render the application again. + + :param values: Any kind of printable object, or formatted string. + :param sep: String inserted between values, default a space. + :param end: String appended after the last value, default a newline. + :param style: :class:`.Style` instance for the color scheme. + :param include_default_pygments_style: `bool`. Include the default Pygments + style when set to `True` (the default). + """ + assert not (output and file) + + # Create Output object. + if output is None: + if file: + output = create_output(stdout=file) + else: + output = get_app_session().output + + assert isinstance(output, Output) + + # Get color depth. + color_depth = color_depth or output.get_default_color_depth() + + # Merges values. + def to_text(val: Any) -> StyleAndTextTuples: + # Normal lists which are not instances of `FormattedText` are + # considered plain text. + if isinstance(val, list) and not isinstance(val, FormattedText): + return to_formatted_text("{0}".format(val)) + return to_formatted_text(val, auto_convert=True) + + fragments = [] + for i, value in enumerate(values): + fragments.extend(to_text(value)) + + if sep and i != len(values) - 1: + fragments.extend(to_text(sep)) + + fragments.extend(to_text(end)) + + # Print output. + def render() -> None: + assert isinstance(output, Output) + + renderer_print_formatted_text( + output, + fragments, + _create_merged_style( + style, include_default_pygments_style=include_default_pygments_style + ), + color_depth=color_depth, + style_transformation=style_transformation, + ) + + # Flush the output stream. + if flush: + output.flush() + + # If an application is running, print above the app. This does not require + # `patch_stdout`. + loop: Optional[AbstractEventLoop] = None + + app = get_app_or_none() + if app is not None: + loop = app.loop + + if loop is not None: + loop.call_soon_threadsafe(lambda: run_in_terminal(render)) + else: + render() + + +def print_container( + container: "AnyContainer", + file: Optional[TextIO] = None, + style: Optional[BaseStyle] = None, + include_default_pygments_style: bool = True, +) -> None: + """ + Print any layout to the output in a non-interactive way. + + Example usage:: + + from prompt_toolkit.widgets import Frame, TextArea + print_container( + Frame(TextArea(text='Hello world!'))) + """ + if file: + output = create_output(stdout=file) + else: + output = get_app_session().output + + def exit_immediately() -> None: + # Use `call_from_executor` to exit "soon", so that we still render one + # initial time, before exiting the application. + get_event_loop().call_soon(lambda: app.exit()) + + app: Application[None] = Application( + layout=Layout(container=container), + output=output, + input=DummyInput(), + style=_create_merged_style( + style, include_default_pygments_style=include_default_pygments_style + ), + ) + app.run(pre_run=exit_immediately, in_thread=True) + + +def _create_merged_style( + style: Optional[BaseStyle], include_default_pygments_style: bool +) -> BaseStyle: + """ + Merge user defined style with built-in style. + """ + styles = [default_ui_style()] + if include_default_pygments_style: + styles.append(default_pygments_style()) + if style: + styles.append(style) + + return merge_styles(styles) + + +def clear() -> None: + """ + Clear the screen. + """ + output = get_app_session().output + output.erase_screen() + output.cursor_goto(0, 0) + output.flush() + + +def set_title(text: str) -> None: + """ + Set the terminal title. + """ + output = get_app_session().output + output.set_title(text) + + +def clear_title() -> None: + """ + Erase the current title. + """ + set_title("") diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/__init__.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/__init__.py index 7c51f0b970..c270ae01bb 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/__init__.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/__init__.py @@ -1,64 +1,64 @@ -""" -Styling for prompt_toolkit applications. -""" -from .base import ( - ANSI_COLOR_NAMES, - DEFAULT_ATTRS, - Attrs, - BaseStyle, - DummyStyle, - DynamicStyle, -) -from .defaults import default_pygments_style, default_ui_style -from .named_colors import NAMED_COLORS -from .pygments import ( - pygments_token_to_classname, - style_from_pygments_cls, - style_from_pygments_dict, -) -from .style import Priority, Style, merge_styles, parse_color -from .style_transformation import ( - AdjustBrightnessStyleTransformation, - ConditionalStyleTransformation, - DummyStyleTransformation, - DynamicStyleTransformation, - ReverseStyleTransformation, - SetDefaultColorStyleTransformation, - StyleTransformation, - SwapLightAndDarkStyleTransformation, - merge_style_transformations, -) - -__all__ = [ - # Base. - "Attrs", - "DEFAULT_ATTRS", - "ANSI_COLOR_NAMES", - "BaseStyle", - "DummyStyle", - "DynamicStyle", - # Defaults. - "default_ui_style", - "default_pygments_style", - # Style. - "Style", - "Priority", - "merge_styles", - "parse_color", - # Style transformation. - "StyleTransformation", - "SwapLightAndDarkStyleTransformation", - "ReverseStyleTransformation", - "SetDefaultColorStyleTransformation", - "AdjustBrightnessStyleTransformation", - "DummyStyleTransformation", - "ConditionalStyleTransformation", - "DynamicStyleTransformation", - "merge_style_transformations", - # Pygments. - "style_from_pygments_cls", - "style_from_pygments_dict", - "pygments_token_to_classname", - # Named colors. - "NAMED_COLORS", -] +""" +Styling for prompt_toolkit applications. +""" +from .base import ( + ANSI_COLOR_NAMES, + DEFAULT_ATTRS, + Attrs, + BaseStyle, + DummyStyle, + DynamicStyle, +) +from .defaults import default_pygments_style, default_ui_style +from .named_colors import NAMED_COLORS +from .pygments import ( + pygments_token_to_classname, + style_from_pygments_cls, + style_from_pygments_dict, +) +from .style import Priority, Style, merge_styles, parse_color +from .style_transformation import ( + AdjustBrightnessStyleTransformation, + ConditionalStyleTransformation, + DummyStyleTransformation, + DynamicStyleTransformation, + ReverseStyleTransformation, + SetDefaultColorStyleTransformation, + StyleTransformation, + SwapLightAndDarkStyleTransformation, + merge_style_transformations, +) + +__all__ = [ + # Base. + "Attrs", + "DEFAULT_ATTRS", + "ANSI_COLOR_NAMES", + "BaseStyle", + "DummyStyle", + "DynamicStyle", + # Defaults. + "default_ui_style", + "default_pygments_style", + # Style. + "Style", + "Priority", + "merge_styles", + "parse_color", + # Style transformation. + "StyleTransformation", + "SwapLightAndDarkStyleTransformation", + "ReverseStyleTransformation", + "SetDefaultColorStyleTransformation", + "AdjustBrightnessStyleTransformation", + "DummyStyleTransformation", + "ConditionalStyleTransformation", + "DynamicStyleTransformation", + "merge_style_transformations", + # Pygments. + "style_from_pygments_cls", + "style_from_pygments_dict", + "pygments_token_to_classname", + # Named colors. + "NAMED_COLORS", +] diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/base.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/base.py index fdfbe14b60..609283b7c6 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/base.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/base.py @@ -1,184 +1,184 @@ -""" -The base classes for the styling. -""" -from abc import ABCMeta, abstractmethod, abstractproperty -from typing import Callable, Dict, Hashable, List, NamedTuple, Optional, Tuple - -__all__ = [ - "Attrs", - "DEFAULT_ATTRS", - "ANSI_COLOR_NAMES", - "ANSI_COLOR_NAMES_ALIASES", - "BaseStyle", - "DummyStyle", - "DynamicStyle", -] - - -#: Style attributes. -Attrs = NamedTuple( - "Attrs", - [ - ("color", Optional[str]), - ("bgcolor", Optional[str]), - ("bold", Optional[bool]), - ("underline", Optional[bool]), - ("strike", Optional[bool]), - ("italic", Optional[bool]), - ("blink", Optional[bool]), - ("reverse", Optional[bool]), - ("hidden", Optional[bool]), - ], -) - -""" -:param color: Hexadecimal string. E.g. '000000' or Ansi color name: e.g. 'ansiblue' -:param bgcolor: Hexadecimal string. E.g. 'ffffff' or Ansi color name: e.g. 'ansired' -:param bold: Boolean -:param underline: Boolean -:param strike: Boolean -:param italic: Boolean -:param blink: Boolean -:param reverse: Boolean -:param hidden: Boolean -""" - -#: The default `Attrs`. -DEFAULT_ATTRS = Attrs( - color="", - bgcolor="", - bold=False, - underline=False, - strike=False, - italic=False, - blink=False, - reverse=False, - hidden=False, -) - - -#: ``Attrs.bgcolor/fgcolor`` can be in either 'ffffff' format, or can be any of -#: the following in case we want to take colors from the 8/16 color palette. -#: Usually, in that case, the terminal application allows to configure the RGB -#: values for these names. -#: ISO 6429 colors -ANSI_COLOR_NAMES = [ - "ansidefault", - # Low intensity, dark. (One or two components 0x80, the other 0x00.) - "ansiblack", - "ansired", - "ansigreen", - "ansiyellow", - "ansiblue", - "ansimagenta", - "ansicyan", - "ansigray", - # High intensity, bright. (One or two components 0xff, the other 0x00. Not supported everywhere.) - "ansibrightblack", - "ansibrightred", - "ansibrightgreen", - "ansibrightyellow", - "ansibrightblue", - "ansibrightmagenta", - "ansibrightcyan", - "ansiwhite", -] - - -# People don't use the same ANSI color names everywhere. In prompt_toolkit 1.0 -# we used some unconventional names (which were contributed like that to -# Pygments). This is fixed now, but we still support the old names. - -# The table below maps the old aliases to the current names. -ANSI_COLOR_NAMES_ALIASES: Dict[str, str] = { - "ansidarkgray": "ansibrightblack", - "ansiteal": "ansicyan", - "ansiturquoise": "ansibrightcyan", - "ansibrown": "ansiyellow", - "ansipurple": "ansimagenta", - "ansifuchsia": "ansibrightmagenta", - "ansilightgray": "ansigray", - "ansidarkred": "ansired", - "ansidarkgreen": "ansigreen", - "ansidarkblue": "ansiblue", -} -assert set(ANSI_COLOR_NAMES_ALIASES.values()).issubset(set(ANSI_COLOR_NAMES)) -assert not (set(ANSI_COLOR_NAMES_ALIASES.keys()) & set(ANSI_COLOR_NAMES)) - - -class BaseStyle(metaclass=ABCMeta): - """ - Abstract base class for prompt_toolkit styles. - """ - - @abstractmethod - def get_attrs_for_style_str( - self, style_str: str, default: Attrs = DEFAULT_ATTRS - ) -> Attrs: - """ - Return :class:`.Attrs` for the given style string. - - :param style_str: The style string. This can contain inline styling as - well as classnames (e.g. "class:title"). - :param default: `Attrs` to be used if no styling was defined. - """ - - @abstractproperty - def style_rules(self) -> List[Tuple[str, str]]: - """ - The list of style rules, used to create this style. - (Required for `DynamicStyle` and `_MergedStyle` to work.) - """ - return [] - - @abstractmethod - def invalidation_hash(self) -> Hashable: - """ - Invalidation hash for the style. When this changes over time, the - renderer knows that something in the style changed, and that everything - has to be redrawn. - """ - - -class DummyStyle(BaseStyle): - """ - A style that doesn't style anything. - """ - - def get_attrs_for_style_str( - self, style_str: str, default: Attrs = DEFAULT_ATTRS - ) -> Attrs: - return default - - def invalidation_hash(self) -> Hashable: - return 1 # Always the same value. - - @property - def style_rules(self) -> List[Tuple[str, str]]: - return [] - - -class DynamicStyle(BaseStyle): - """ - Style class that can dynamically returns an other Style. - - :param get_style: Callable that returns a :class:`.Style` instance. - """ - - def __init__(self, get_style: Callable[[], Optional[BaseStyle]]): - self.get_style = get_style - self._dummy = DummyStyle() - - def get_attrs_for_style_str( - self, style_str: str, default: Attrs = DEFAULT_ATTRS - ) -> Attrs: - style = self.get_style() or self._dummy - - return style.get_attrs_for_style_str(style_str, default) - - def invalidation_hash(self) -> Hashable: - return (self.get_style() or self._dummy).invalidation_hash() - - @property - def style_rules(self) -> List[Tuple[str, str]]: - return (self.get_style() or self._dummy).style_rules +""" +The base classes for the styling. +""" +from abc import ABCMeta, abstractmethod, abstractproperty +from typing import Callable, Dict, Hashable, List, NamedTuple, Optional, Tuple + +__all__ = [ + "Attrs", + "DEFAULT_ATTRS", + "ANSI_COLOR_NAMES", + "ANSI_COLOR_NAMES_ALIASES", + "BaseStyle", + "DummyStyle", + "DynamicStyle", +] + + +#: Style attributes. +Attrs = NamedTuple( + "Attrs", + [ + ("color", Optional[str]), + ("bgcolor", Optional[str]), + ("bold", Optional[bool]), + ("underline", Optional[bool]), + ("strike", Optional[bool]), + ("italic", Optional[bool]), + ("blink", Optional[bool]), + ("reverse", Optional[bool]), + ("hidden", Optional[bool]), + ], +) + +""" +:param color: Hexadecimal string. E.g. '000000' or Ansi color name: e.g. 'ansiblue' +:param bgcolor: Hexadecimal string. E.g. 'ffffff' or Ansi color name: e.g. 'ansired' +:param bold: Boolean +:param underline: Boolean +:param strike: Boolean +:param italic: Boolean +:param blink: Boolean +:param reverse: Boolean +:param hidden: Boolean +""" + +#: The default `Attrs`. +DEFAULT_ATTRS = Attrs( + color="", + bgcolor="", + bold=False, + underline=False, + strike=False, + italic=False, + blink=False, + reverse=False, + hidden=False, +) + + +#: ``Attrs.bgcolor/fgcolor`` can be in either 'ffffff' format, or can be any of +#: the following in case we want to take colors from the 8/16 color palette. +#: Usually, in that case, the terminal application allows to configure the RGB +#: values for these names. +#: ISO 6429 colors +ANSI_COLOR_NAMES = [ + "ansidefault", + # Low intensity, dark. (One or two components 0x80, the other 0x00.) + "ansiblack", + "ansired", + "ansigreen", + "ansiyellow", + "ansiblue", + "ansimagenta", + "ansicyan", + "ansigray", + # High intensity, bright. (One or two components 0xff, the other 0x00. Not supported everywhere.) + "ansibrightblack", + "ansibrightred", + "ansibrightgreen", + "ansibrightyellow", + "ansibrightblue", + "ansibrightmagenta", + "ansibrightcyan", + "ansiwhite", +] + + +# People don't use the same ANSI color names everywhere. In prompt_toolkit 1.0 +# we used some unconventional names (which were contributed like that to +# Pygments). This is fixed now, but we still support the old names. + +# The table below maps the old aliases to the current names. +ANSI_COLOR_NAMES_ALIASES: Dict[str, str] = { + "ansidarkgray": "ansibrightblack", + "ansiteal": "ansicyan", + "ansiturquoise": "ansibrightcyan", + "ansibrown": "ansiyellow", + "ansipurple": "ansimagenta", + "ansifuchsia": "ansibrightmagenta", + "ansilightgray": "ansigray", + "ansidarkred": "ansired", + "ansidarkgreen": "ansigreen", + "ansidarkblue": "ansiblue", +} +assert set(ANSI_COLOR_NAMES_ALIASES.values()).issubset(set(ANSI_COLOR_NAMES)) +assert not (set(ANSI_COLOR_NAMES_ALIASES.keys()) & set(ANSI_COLOR_NAMES)) + + +class BaseStyle(metaclass=ABCMeta): + """ + Abstract base class for prompt_toolkit styles. + """ + + @abstractmethod + def get_attrs_for_style_str( + self, style_str: str, default: Attrs = DEFAULT_ATTRS + ) -> Attrs: + """ + Return :class:`.Attrs` for the given style string. + + :param style_str: The style string. This can contain inline styling as + well as classnames (e.g. "class:title"). + :param default: `Attrs` to be used if no styling was defined. + """ + + @abstractproperty + def style_rules(self) -> List[Tuple[str, str]]: + """ + The list of style rules, used to create this style. + (Required for `DynamicStyle` and `_MergedStyle` to work.) + """ + return [] + + @abstractmethod + def invalidation_hash(self) -> Hashable: + """ + Invalidation hash for the style. When this changes over time, the + renderer knows that something in the style changed, and that everything + has to be redrawn. + """ + + +class DummyStyle(BaseStyle): + """ + A style that doesn't style anything. + """ + + def get_attrs_for_style_str( + self, style_str: str, default: Attrs = DEFAULT_ATTRS + ) -> Attrs: + return default + + def invalidation_hash(self) -> Hashable: + return 1 # Always the same value. + + @property + def style_rules(self) -> List[Tuple[str, str]]: + return [] + + +class DynamicStyle(BaseStyle): + """ + Style class that can dynamically returns an other Style. + + :param get_style: Callable that returns a :class:`.Style` instance. + """ + + def __init__(self, get_style: Callable[[], Optional[BaseStyle]]): + self.get_style = get_style + self._dummy = DummyStyle() + + def get_attrs_for_style_str( + self, style_str: str, default: Attrs = DEFAULT_ATTRS + ) -> Attrs: + style = self.get_style() or self._dummy + + return style.get_attrs_for_style_str(style_str, default) + + def invalidation_hash(self) -> Hashable: + return (self.get_style() or self._dummy).invalidation_hash() + + @property + def style_rules(self) -> List[Tuple[str, str]]: + return (self.get_style() or self._dummy).style_rules diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/defaults.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/defaults.py index 148b73587b..4ac554562c 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/defaults.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/defaults.py @@ -1,231 +1,231 @@ -""" -The default styling. -""" -from prompt_toolkit.cache import memoized - -from .base import ANSI_COLOR_NAMES, BaseStyle -from .named_colors import NAMED_COLORS -from .style import Style, merge_styles - -__all__ = [ - "default_ui_style", - "default_pygments_style", -] - -#: Default styling. Mapping from classnames to their style definition. -PROMPT_TOOLKIT_STYLE = [ - # Highlighting of search matches in document. - ("search", "bg:ansibrightyellow ansiblack"), - ("search.current", ""), - # Incremental search. - ("incsearch", ""), - ("incsearch.current", "reverse"), - # Highlighting of select text in document. - ("selected", "reverse"), - ("cursor-column", "bg:#dddddd"), - ("cursor-line", "underline"), - ("color-column", "bg:#ccaacc"), - # Highlighting of matching brackets. - ("matching-bracket", ""), - ("matching-bracket.other", "#000000 bg:#aacccc"), - ("matching-bracket.cursor", "#ff8888 bg:#880000"), - # Styling of other cursors, in case of block editing. - ("multiple-cursors", "#000000 bg:#ccccaa"), - # Line numbers. - ("line-number", "#888888"), - ("line-number.current", "bold"), - ("tilde", "#8888ff"), - # Default prompt. - ("prompt", ""), - ("prompt.arg", "noinherit"), - ("prompt.arg.text", ""), - ("prompt.search", "noinherit"), - ("prompt.search.text", ""), - # Search toolbar. - ("search-toolbar", "bold"), - ("search-toolbar.text", "nobold"), - # System toolbar - ("system-toolbar", "bold"), - ("system-toolbar.text", "nobold"), - # "arg" toolbar. - ("arg-toolbar", "bold"), - ("arg-toolbar.text", "nobold"), - # Validation toolbar. - ("validation-toolbar", "bg:#550000 #ffffff"), - ("window-too-small", "bg:#550000 #ffffff"), - # Completions toolbar. - ("completion-toolbar", "bg:#bbbbbb #000000"), - ("completion-toolbar.arrow", "bg:#bbbbbb #000000 bold"), - ("completion-toolbar.completion", "bg:#bbbbbb #000000"), - ("completion-toolbar.completion.current", "bg:#444444 #ffffff"), - # Completions menu. - ("completion-menu", "bg:#bbbbbb #000000"), - ("completion-menu.completion", ""), - ("completion-menu.completion.current", "bg:#888888 #ffffff"), - ("completion-menu.meta.completion", "bg:#999999 #000000"), - ("completion-menu.meta.completion.current", "bg:#aaaaaa #000000"), - ("completion-menu.multi-column-meta", "bg:#aaaaaa #000000"), - # Fuzzy matches in completion menu (for FuzzyCompleter). - ("completion-menu.completion fuzzymatch.outside", "fg:#444444"), - ("completion-menu.completion fuzzymatch.inside", "bold"), - ("completion-menu.completion fuzzymatch.inside.character", "underline"), - ("completion-menu.completion.current fuzzymatch.outside", "fg:default"), - ("completion-menu.completion.current fuzzymatch.inside", "nobold"), - # Styling of readline-like completions. - ("readline-like-completions", ""), - ("readline-like-completions.completion", ""), - ("readline-like-completions.completion fuzzymatch.outside", "#888888"), - ("readline-like-completions.completion fuzzymatch.inside", ""), - ("readline-like-completions.completion fuzzymatch.inside.character", "underline"), - # Scrollbars. - ("scrollbar.background", "bg:#aaaaaa"), - ("scrollbar.button", "bg:#444444"), - ("scrollbar.arrow", "noinherit bold"), - # Start/end of scrollbars. Adding 'underline' here provides a nice little - # detail to the progress bar, but it doesn't look good on all terminals. - # ('scrollbar.start', 'underline #ffffff'), - # ('scrollbar.end', 'underline #000000'), - # Auto suggestion text. - ("auto-suggestion", "#666666"), - # Trailing whitespace and tabs. - ("trailing-whitespace", "#999999"), - ("tab", "#999999"), - # When Control-C/D has been pressed. Grayed. - ("aborting", "#888888 bg:default noreverse noitalic nounderline noblink"), - ("exiting", "#888888 bg:default noreverse noitalic nounderline noblink"), - # Entering a Vi digraph. - ("digraph", "#4444ff"), - # Control characters, like ^C, ^X. - ("control-character", "ansiblue"), - # Non-breaking space. - ("nbsp", "underline ansiyellow"), - # Default styling of HTML elements. - ("i", "italic"), - ("u", "underline"), - ("s", "strike"), - ("b", "bold"), - ("em", "italic"), - ("strong", "bold"), - ("del", "strike"), - ("hidden", "hidden"), - # It should be possible to use the style names in HTML. - # <reverse>...</reverse> or <noreverse>...</noreverse>. - ("italic", "italic"), - ("underline", "underline"), - ("strike", "strike"), - ("bold", "bold"), - ("reverse", "reverse"), - ("noitalic", "noitalic"), - ("nounderline", "nounderline"), - ("nostrike", "nostrike"), - ("nobold", "nobold"), - ("noreverse", "noreverse"), - # Prompt bottom toolbar - ("bottom-toolbar", "reverse"), -] - - -# Style that will turn for instance the class 'red' into 'red'. -COLORS_STYLE = [(name, "fg:" + name) for name in ANSI_COLOR_NAMES] + [ - (name.lower(), "fg:" + name) for name in NAMED_COLORS -] - - -WIDGETS_STYLE = [ - # Dialog windows. - ("dialog", "bg:#4444ff"), - ("dialog.body", "bg:#ffffff #000000"), - ("dialog.body text-area", "bg:#cccccc"), - ("dialog.body text-area last-line", "underline"), - ("dialog frame.label", "#ff0000 bold"), - # Scrollbars in dialogs. - ("dialog.body scrollbar.background", ""), - ("dialog.body scrollbar.button", "bg:#000000"), - ("dialog.body scrollbar.arrow", ""), - ("dialog.body scrollbar.start", "nounderline"), - ("dialog.body scrollbar.end", "nounderline"), - # Buttons. - ("button", ""), - ("button.arrow", "bold"), - ("button.focused", "bg:#aa0000 #ffffff"), - # Menu bars. - ("menu-bar", "bg:#aaaaaa #000000"), - ("menu-bar.selected-item", "bg:#ffffff #000000"), - ("menu", "bg:#888888 #ffffff"), - ("menu.border", "#aaaaaa"), - ("menu.border shadow", "#444444"), - # Shadows. - ("dialog shadow", "bg:#000088"), - ("dialog.body shadow", "bg:#aaaaaa"), - ("progress-bar", "bg:#000088"), - ("progress-bar.used", "bg:#ff0000"), -] - - -# The default Pygments style, include this by default in case a Pygments lexer -# is used. -PYGMENTS_DEFAULT_STYLE = { - "pygments.whitespace": "#bbbbbb", - "pygments.comment": "italic #408080", - "pygments.comment.preproc": "noitalic #bc7a00", - "pygments.keyword": "bold #008000", - "pygments.keyword.pseudo": "nobold", - "pygments.keyword.type": "nobold #b00040", - "pygments.operator": "#666666", - "pygments.operator.word": "bold #aa22ff", - "pygments.name.builtin": "#008000", - "pygments.name.function": "#0000ff", - "pygments.name.class": "bold #0000ff", - "pygments.name.namespace": "bold #0000ff", - "pygments.name.exception": "bold #d2413a", - "pygments.name.variable": "#19177c", - "pygments.name.constant": "#880000", - "pygments.name.label": "#a0a000", - "pygments.name.entity": "bold #999999", - "pygments.name.attribute": "#7d9029", - "pygments.name.tag": "bold #008000", - "pygments.name.decorator": "#aa22ff", - # Note: In Pygments, Token.String is an alias for Token.Literal.String, - # and Token.Number as an alias for Token.Literal.Number. - "pygments.literal.string": "#ba2121", - "pygments.literal.string.doc": "italic", - "pygments.literal.string.interpol": "bold #bb6688", - "pygments.literal.string.escape": "bold #bb6622", - "pygments.literal.string.regex": "#bb6688", - "pygments.literal.string.symbol": "#19177c", - "pygments.literal.string.other": "#008000", - "pygments.literal.number": "#666666", - "pygments.generic.heading": "bold #000080", - "pygments.generic.subheading": "bold #800080", - "pygments.generic.deleted": "#a00000", - "pygments.generic.inserted": "#00a000", - "pygments.generic.error": "#ff0000", - "pygments.generic.emph": "italic", - "pygments.generic.strong": "bold", - "pygments.generic.prompt": "bold #000080", - "pygments.generic.output": "#888", - "pygments.generic.traceback": "#04d", - "pygments.error": "border:#ff0000", -} - - -@memoized() -def default_ui_style() -> BaseStyle: - """ - Create a default `Style` object. - """ - return merge_styles( - [ - Style(PROMPT_TOOLKIT_STYLE), - Style(COLORS_STYLE), - Style(WIDGETS_STYLE), - ] - ) - - -@memoized() -def default_pygments_style() -> Style: - """ - Create a `Style` object that contains the default Pygments style. - """ - return Style.from_dict(PYGMENTS_DEFAULT_STYLE) +""" +The default styling. +""" +from prompt_toolkit.cache import memoized + +from .base import ANSI_COLOR_NAMES, BaseStyle +from .named_colors import NAMED_COLORS +from .style import Style, merge_styles + +__all__ = [ + "default_ui_style", + "default_pygments_style", +] + +#: Default styling. Mapping from classnames to their style definition. +PROMPT_TOOLKIT_STYLE = [ + # Highlighting of search matches in document. + ("search", "bg:ansibrightyellow ansiblack"), + ("search.current", ""), + # Incremental search. + ("incsearch", ""), + ("incsearch.current", "reverse"), + # Highlighting of select text in document. + ("selected", "reverse"), + ("cursor-column", "bg:#dddddd"), + ("cursor-line", "underline"), + ("color-column", "bg:#ccaacc"), + # Highlighting of matching brackets. + ("matching-bracket", ""), + ("matching-bracket.other", "#000000 bg:#aacccc"), + ("matching-bracket.cursor", "#ff8888 bg:#880000"), + # Styling of other cursors, in case of block editing. + ("multiple-cursors", "#000000 bg:#ccccaa"), + # Line numbers. + ("line-number", "#888888"), + ("line-number.current", "bold"), + ("tilde", "#8888ff"), + # Default prompt. + ("prompt", ""), + ("prompt.arg", "noinherit"), + ("prompt.arg.text", ""), + ("prompt.search", "noinherit"), + ("prompt.search.text", ""), + # Search toolbar. + ("search-toolbar", "bold"), + ("search-toolbar.text", "nobold"), + # System toolbar + ("system-toolbar", "bold"), + ("system-toolbar.text", "nobold"), + # "arg" toolbar. + ("arg-toolbar", "bold"), + ("arg-toolbar.text", "nobold"), + # Validation toolbar. + ("validation-toolbar", "bg:#550000 #ffffff"), + ("window-too-small", "bg:#550000 #ffffff"), + # Completions toolbar. + ("completion-toolbar", "bg:#bbbbbb #000000"), + ("completion-toolbar.arrow", "bg:#bbbbbb #000000 bold"), + ("completion-toolbar.completion", "bg:#bbbbbb #000000"), + ("completion-toolbar.completion.current", "bg:#444444 #ffffff"), + # Completions menu. + ("completion-menu", "bg:#bbbbbb #000000"), + ("completion-menu.completion", ""), + ("completion-menu.completion.current", "bg:#888888 #ffffff"), + ("completion-menu.meta.completion", "bg:#999999 #000000"), + ("completion-menu.meta.completion.current", "bg:#aaaaaa #000000"), + ("completion-menu.multi-column-meta", "bg:#aaaaaa #000000"), + # Fuzzy matches in completion menu (for FuzzyCompleter). + ("completion-menu.completion fuzzymatch.outside", "fg:#444444"), + ("completion-menu.completion fuzzymatch.inside", "bold"), + ("completion-menu.completion fuzzymatch.inside.character", "underline"), + ("completion-menu.completion.current fuzzymatch.outside", "fg:default"), + ("completion-menu.completion.current fuzzymatch.inside", "nobold"), + # Styling of readline-like completions. + ("readline-like-completions", ""), + ("readline-like-completions.completion", ""), + ("readline-like-completions.completion fuzzymatch.outside", "#888888"), + ("readline-like-completions.completion fuzzymatch.inside", ""), + ("readline-like-completions.completion fuzzymatch.inside.character", "underline"), + # Scrollbars. + ("scrollbar.background", "bg:#aaaaaa"), + ("scrollbar.button", "bg:#444444"), + ("scrollbar.arrow", "noinherit bold"), + # Start/end of scrollbars. Adding 'underline' here provides a nice little + # detail to the progress bar, but it doesn't look good on all terminals. + # ('scrollbar.start', 'underline #ffffff'), + # ('scrollbar.end', 'underline #000000'), + # Auto suggestion text. + ("auto-suggestion", "#666666"), + # Trailing whitespace and tabs. + ("trailing-whitespace", "#999999"), + ("tab", "#999999"), + # When Control-C/D has been pressed. Grayed. + ("aborting", "#888888 bg:default noreverse noitalic nounderline noblink"), + ("exiting", "#888888 bg:default noreverse noitalic nounderline noblink"), + # Entering a Vi digraph. + ("digraph", "#4444ff"), + # Control characters, like ^C, ^X. + ("control-character", "ansiblue"), + # Non-breaking space. + ("nbsp", "underline ansiyellow"), + # Default styling of HTML elements. + ("i", "italic"), + ("u", "underline"), + ("s", "strike"), + ("b", "bold"), + ("em", "italic"), + ("strong", "bold"), + ("del", "strike"), + ("hidden", "hidden"), + # It should be possible to use the style names in HTML. + # <reverse>...</reverse> or <noreverse>...</noreverse>. + ("italic", "italic"), + ("underline", "underline"), + ("strike", "strike"), + ("bold", "bold"), + ("reverse", "reverse"), + ("noitalic", "noitalic"), + ("nounderline", "nounderline"), + ("nostrike", "nostrike"), + ("nobold", "nobold"), + ("noreverse", "noreverse"), + # Prompt bottom toolbar + ("bottom-toolbar", "reverse"), +] + + +# Style that will turn for instance the class 'red' into 'red'. +COLORS_STYLE = [(name, "fg:" + name) for name in ANSI_COLOR_NAMES] + [ + (name.lower(), "fg:" + name) for name in NAMED_COLORS +] + + +WIDGETS_STYLE = [ + # Dialog windows. + ("dialog", "bg:#4444ff"), + ("dialog.body", "bg:#ffffff #000000"), + ("dialog.body text-area", "bg:#cccccc"), + ("dialog.body text-area last-line", "underline"), + ("dialog frame.label", "#ff0000 bold"), + # Scrollbars in dialogs. + ("dialog.body scrollbar.background", ""), + ("dialog.body scrollbar.button", "bg:#000000"), + ("dialog.body scrollbar.arrow", ""), + ("dialog.body scrollbar.start", "nounderline"), + ("dialog.body scrollbar.end", "nounderline"), + # Buttons. + ("button", ""), + ("button.arrow", "bold"), + ("button.focused", "bg:#aa0000 #ffffff"), + # Menu bars. + ("menu-bar", "bg:#aaaaaa #000000"), + ("menu-bar.selected-item", "bg:#ffffff #000000"), + ("menu", "bg:#888888 #ffffff"), + ("menu.border", "#aaaaaa"), + ("menu.border shadow", "#444444"), + # Shadows. + ("dialog shadow", "bg:#000088"), + ("dialog.body shadow", "bg:#aaaaaa"), + ("progress-bar", "bg:#000088"), + ("progress-bar.used", "bg:#ff0000"), +] + + +# The default Pygments style, include this by default in case a Pygments lexer +# is used. +PYGMENTS_DEFAULT_STYLE = { + "pygments.whitespace": "#bbbbbb", + "pygments.comment": "italic #408080", + "pygments.comment.preproc": "noitalic #bc7a00", + "pygments.keyword": "bold #008000", + "pygments.keyword.pseudo": "nobold", + "pygments.keyword.type": "nobold #b00040", + "pygments.operator": "#666666", + "pygments.operator.word": "bold #aa22ff", + "pygments.name.builtin": "#008000", + "pygments.name.function": "#0000ff", + "pygments.name.class": "bold #0000ff", + "pygments.name.namespace": "bold #0000ff", + "pygments.name.exception": "bold #d2413a", + "pygments.name.variable": "#19177c", + "pygments.name.constant": "#880000", + "pygments.name.label": "#a0a000", + "pygments.name.entity": "bold #999999", + "pygments.name.attribute": "#7d9029", + "pygments.name.tag": "bold #008000", + "pygments.name.decorator": "#aa22ff", + # Note: In Pygments, Token.String is an alias for Token.Literal.String, + # and Token.Number as an alias for Token.Literal.Number. + "pygments.literal.string": "#ba2121", + "pygments.literal.string.doc": "italic", + "pygments.literal.string.interpol": "bold #bb6688", + "pygments.literal.string.escape": "bold #bb6622", + "pygments.literal.string.regex": "#bb6688", + "pygments.literal.string.symbol": "#19177c", + "pygments.literal.string.other": "#008000", + "pygments.literal.number": "#666666", + "pygments.generic.heading": "bold #000080", + "pygments.generic.subheading": "bold #800080", + "pygments.generic.deleted": "#a00000", + "pygments.generic.inserted": "#00a000", + "pygments.generic.error": "#ff0000", + "pygments.generic.emph": "italic", + "pygments.generic.strong": "bold", + "pygments.generic.prompt": "bold #000080", + "pygments.generic.output": "#888", + "pygments.generic.traceback": "#04d", + "pygments.error": "border:#ff0000", +} + + +@memoized() +def default_ui_style() -> BaseStyle: + """ + Create a default `Style` object. + """ + return merge_styles( + [ + Style(PROMPT_TOOLKIT_STYLE), + Style(COLORS_STYLE), + Style(WIDGETS_STYLE), + ] + ) + + +@memoized() +def default_pygments_style() -> Style: + """ + Create a `Style` object that contains the default Pygments style. + """ + return Style.from_dict(PYGMENTS_DEFAULT_STYLE) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/named_colors.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/named_colors.py index 9fcde6403f..37bd6de446 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/named_colors.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/named_colors.py @@ -1,161 +1,161 @@ -""" -All modern web browsers support these 140 color names. -Taken from: https://www.w3schools.com/colors/colors_names.asp -""" -from typing import Dict - -__all__ = [ - "NAMED_COLORS", -] - - -NAMED_COLORS: Dict[str, str] = { - "AliceBlue": "#f0f8ff", - "AntiqueWhite": "#faebd7", - "Aqua": "#00ffff", - "Aquamarine": "#7fffd4", - "Azure": "#f0ffff", - "Beige": "#f5f5dc", - "Bisque": "#ffe4c4", - "Black": "#000000", - "BlanchedAlmond": "#ffebcd", - "Blue": "#0000ff", - "BlueViolet": "#8a2be2", - "Brown": "#a52a2a", - "BurlyWood": "#deb887", - "CadetBlue": "#5f9ea0", - "Chartreuse": "#7fff00", - "Chocolate": "#d2691e", - "Coral": "#ff7f50", - "CornflowerBlue": "#6495ed", - "Cornsilk": "#fff8dc", - "Crimson": "#dc143c", - "Cyan": "#00ffff", - "DarkBlue": "#00008b", - "DarkCyan": "#008b8b", - "DarkGoldenRod": "#b8860b", - "DarkGray": "#a9a9a9", - "DarkGreen": "#006400", - "DarkGrey": "#a9a9a9", - "DarkKhaki": "#bdb76b", - "DarkMagenta": "#8b008b", - "DarkOliveGreen": "#556b2f", - "DarkOrange": "#ff8c00", - "DarkOrchid": "#9932cc", - "DarkRed": "#8b0000", - "DarkSalmon": "#e9967a", - "DarkSeaGreen": "#8fbc8f", - "DarkSlateBlue": "#483d8b", - "DarkSlateGray": "#2f4f4f", - "DarkSlateGrey": "#2f4f4f", - "DarkTurquoise": "#00ced1", - "DarkViolet": "#9400d3", - "DeepPink": "#ff1493", - "DeepSkyBlue": "#00bfff", - "DimGray": "#696969", - "DimGrey": "#696969", - "DodgerBlue": "#1e90ff", - "FireBrick": "#b22222", - "FloralWhite": "#fffaf0", - "ForestGreen": "#228b22", - "Fuchsia": "#ff00ff", - "Gainsboro": "#dcdcdc", - "GhostWhite": "#f8f8ff", - "Gold": "#ffd700", - "GoldenRod": "#daa520", - "Gray": "#808080", - "Green": "#008000", - "GreenYellow": "#adff2f", - "Grey": "#808080", - "HoneyDew": "#f0fff0", - "HotPink": "#ff69b4", - "IndianRed": "#cd5c5c", - "Indigo": "#4b0082", - "Ivory": "#fffff0", - "Khaki": "#f0e68c", - "Lavender": "#e6e6fa", - "LavenderBlush": "#fff0f5", - "LawnGreen": "#7cfc00", - "LemonChiffon": "#fffacd", - "LightBlue": "#add8e6", - "LightCoral": "#f08080", - "LightCyan": "#e0ffff", - "LightGoldenRodYellow": "#fafad2", - "LightGray": "#d3d3d3", - "LightGreen": "#90ee90", - "LightGrey": "#d3d3d3", - "LightPink": "#ffb6c1", - "LightSalmon": "#ffa07a", - "LightSeaGreen": "#20b2aa", - "LightSkyBlue": "#87cefa", - "LightSlateGray": "#778899", - "LightSlateGrey": "#778899", - "LightSteelBlue": "#b0c4de", - "LightYellow": "#ffffe0", - "Lime": "#00ff00", - "LimeGreen": "#32cd32", - "Linen": "#faf0e6", - "Magenta": "#ff00ff", - "Maroon": "#800000", - "MediumAquaMarine": "#66cdaa", - "MediumBlue": "#0000cd", - "MediumOrchid": "#ba55d3", - "MediumPurple": "#9370db", - "MediumSeaGreen": "#3cb371", - "MediumSlateBlue": "#7b68ee", - "MediumSpringGreen": "#00fa9a", - "MediumTurquoise": "#48d1cc", - "MediumVioletRed": "#c71585", - "MidnightBlue": "#191970", - "MintCream": "#f5fffa", - "MistyRose": "#ffe4e1", - "Moccasin": "#ffe4b5", - "NavajoWhite": "#ffdead", - "Navy": "#000080", - "OldLace": "#fdf5e6", - "Olive": "#808000", - "OliveDrab": "#6b8e23", - "Orange": "#ffa500", - "OrangeRed": "#ff4500", - "Orchid": "#da70d6", - "PaleGoldenRod": "#eee8aa", - "PaleGreen": "#98fb98", - "PaleTurquoise": "#afeeee", - "PaleVioletRed": "#db7093", - "PapayaWhip": "#ffefd5", - "PeachPuff": "#ffdab9", - "Peru": "#cd853f", - "Pink": "#ffc0cb", - "Plum": "#dda0dd", - "PowderBlue": "#b0e0e6", - "Purple": "#800080", - "RebeccaPurple": "#663399", - "Red": "#ff0000", - "RosyBrown": "#bc8f8f", - "RoyalBlue": "#4169e1", - "SaddleBrown": "#8b4513", - "Salmon": "#fa8072", - "SandyBrown": "#f4a460", - "SeaGreen": "#2e8b57", - "SeaShell": "#fff5ee", - "Sienna": "#a0522d", - "Silver": "#c0c0c0", - "SkyBlue": "#87ceeb", - "SlateBlue": "#6a5acd", - "SlateGray": "#708090", - "SlateGrey": "#708090", - "Snow": "#fffafa", - "SpringGreen": "#00ff7f", - "SteelBlue": "#4682b4", - "Tan": "#d2b48c", - "Teal": "#008080", - "Thistle": "#d8bfd8", - "Tomato": "#ff6347", - "Turquoise": "#40e0d0", - "Violet": "#ee82ee", - "Wheat": "#f5deb3", - "White": "#ffffff", - "WhiteSmoke": "#f5f5f5", - "Yellow": "#ffff00", - "YellowGreen": "#9acd32", -} +""" +All modern web browsers support these 140 color names. +Taken from: https://www.w3schools.com/colors/colors_names.asp +""" +from typing import Dict + +__all__ = [ + "NAMED_COLORS", +] + + +NAMED_COLORS: Dict[str, str] = { + "AliceBlue": "#f0f8ff", + "AntiqueWhite": "#faebd7", + "Aqua": "#00ffff", + "Aquamarine": "#7fffd4", + "Azure": "#f0ffff", + "Beige": "#f5f5dc", + "Bisque": "#ffe4c4", + "Black": "#000000", + "BlanchedAlmond": "#ffebcd", + "Blue": "#0000ff", + "BlueViolet": "#8a2be2", + "Brown": "#a52a2a", + "BurlyWood": "#deb887", + "CadetBlue": "#5f9ea0", + "Chartreuse": "#7fff00", + "Chocolate": "#d2691e", + "Coral": "#ff7f50", + "CornflowerBlue": "#6495ed", + "Cornsilk": "#fff8dc", + "Crimson": "#dc143c", + "Cyan": "#00ffff", + "DarkBlue": "#00008b", + "DarkCyan": "#008b8b", + "DarkGoldenRod": "#b8860b", + "DarkGray": "#a9a9a9", + "DarkGreen": "#006400", + "DarkGrey": "#a9a9a9", + "DarkKhaki": "#bdb76b", + "DarkMagenta": "#8b008b", + "DarkOliveGreen": "#556b2f", + "DarkOrange": "#ff8c00", + "DarkOrchid": "#9932cc", + "DarkRed": "#8b0000", + "DarkSalmon": "#e9967a", + "DarkSeaGreen": "#8fbc8f", + "DarkSlateBlue": "#483d8b", + "DarkSlateGray": "#2f4f4f", + "DarkSlateGrey": "#2f4f4f", + "DarkTurquoise": "#00ced1", + "DarkViolet": "#9400d3", + "DeepPink": "#ff1493", + "DeepSkyBlue": "#00bfff", + "DimGray": "#696969", + "DimGrey": "#696969", + "DodgerBlue": "#1e90ff", + "FireBrick": "#b22222", + "FloralWhite": "#fffaf0", + "ForestGreen": "#228b22", + "Fuchsia": "#ff00ff", + "Gainsboro": "#dcdcdc", + "GhostWhite": "#f8f8ff", + "Gold": "#ffd700", + "GoldenRod": "#daa520", + "Gray": "#808080", + "Green": "#008000", + "GreenYellow": "#adff2f", + "Grey": "#808080", + "HoneyDew": "#f0fff0", + "HotPink": "#ff69b4", + "IndianRed": "#cd5c5c", + "Indigo": "#4b0082", + "Ivory": "#fffff0", + "Khaki": "#f0e68c", + "Lavender": "#e6e6fa", + "LavenderBlush": "#fff0f5", + "LawnGreen": "#7cfc00", + "LemonChiffon": "#fffacd", + "LightBlue": "#add8e6", + "LightCoral": "#f08080", + "LightCyan": "#e0ffff", + "LightGoldenRodYellow": "#fafad2", + "LightGray": "#d3d3d3", + "LightGreen": "#90ee90", + "LightGrey": "#d3d3d3", + "LightPink": "#ffb6c1", + "LightSalmon": "#ffa07a", + "LightSeaGreen": "#20b2aa", + "LightSkyBlue": "#87cefa", + "LightSlateGray": "#778899", + "LightSlateGrey": "#778899", + "LightSteelBlue": "#b0c4de", + "LightYellow": "#ffffe0", + "Lime": "#00ff00", + "LimeGreen": "#32cd32", + "Linen": "#faf0e6", + "Magenta": "#ff00ff", + "Maroon": "#800000", + "MediumAquaMarine": "#66cdaa", + "MediumBlue": "#0000cd", + "MediumOrchid": "#ba55d3", + "MediumPurple": "#9370db", + "MediumSeaGreen": "#3cb371", + "MediumSlateBlue": "#7b68ee", + "MediumSpringGreen": "#00fa9a", + "MediumTurquoise": "#48d1cc", + "MediumVioletRed": "#c71585", + "MidnightBlue": "#191970", + "MintCream": "#f5fffa", + "MistyRose": "#ffe4e1", + "Moccasin": "#ffe4b5", + "NavajoWhite": "#ffdead", + "Navy": "#000080", + "OldLace": "#fdf5e6", + "Olive": "#808000", + "OliveDrab": "#6b8e23", + "Orange": "#ffa500", + "OrangeRed": "#ff4500", + "Orchid": "#da70d6", + "PaleGoldenRod": "#eee8aa", + "PaleGreen": "#98fb98", + "PaleTurquoise": "#afeeee", + "PaleVioletRed": "#db7093", + "PapayaWhip": "#ffefd5", + "PeachPuff": "#ffdab9", + "Peru": "#cd853f", + "Pink": "#ffc0cb", + "Plum": "#dda0dd", + "PowderBlue": "#b0e0e6", + "Purple": "#800080", + "RebeccaPurple": "#663399", + "Red": "#ff0000", + "RosyBrown": "#bc8f8f", + "RoyalBlue": "#4169e1", + "SaddleBrown": "#8b4513", + "Salmon": "#fa8072", + "SandyBrown": "#f4a460", + "SeaGreen": "#2e8b57", + "SeaShell": "#fff5ee", + "Sienna": "#a0522d", + "Silver": "#c0c0c0", + "SkyBlue": "#87ceeb", + "SlateBlue": "#6a5acd", + "SlateGray": "#708090", + "SlateGrey": "#708090", + "Snow": "#fffafa", + "SpringGreen": "#00ff7f", + "SteelBlue": "#4682b4", + "Tan": "#d2b48c", + "Teal": "#008080", + "Thistle": "#d8bfd8", + "Tomato": "#ff6347", + "Turquoise": "#40e0d0", + "Violet": "#ee82ee", + "Wheat": "#f5deb3", + "White": "#ffffff", + "WhiteSmoke": "#f5f5f5", + "Yellow": "#ffff00", + "YellowGreen": "#9acd32", +} diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/pygments.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/pygments.py index d3d0024b7c..382e5e315b 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/pygments.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/pygments.py @@ -1,67 +1,67 @@ -""" -Adaptor for building prompt_toolkit styles, starting from a Pygments style. - -Usage:: - - from pygments.styles.tango import TangoStyle - style = style_from_pygments_cls(pygments_style_cls=TangoStyle) -""" -from typing import TYPE_CHECKING, Dict, Type - -from .style import Style - -if TYPE_CHECKING: - from pygments.style import Style as PygmentsStyle - from pygments.token import Token - - -__all__ = [ - "style_from_pygments_cls", - "style_from_pygments_dict", - "pygments_token_to_classname", -] - - -def style_from_pygments_cls(pygments_style_cls: Type["PygmentsStyle"]) -> Style: - """ - Shortcut to create a :class:`.Style` instance from a Pygments style class - and a style dictionary. - - Example:: - - from prompt_toolkit.styles.from_pygments import style_from_pygments_cls - from pygments.styles import get_style_by_name - style = style_from_pygments_cls(get_style_by_name('monokai')) - - :param pygments_style_cls: Pygments style class to start from. - """ - # Import inline. - from pygments.style import Style as PygmentsStyle - - assert issubclass(pygments_style_cls, PygmentsStyle) - - return style_from_pygments_dict(pygments_style_cls.styles) - - -def style_from_pygments_dict(pygments_dict: Dict["Token", str]) -> Style: - """ - Create a :class:`.Style` instance from a Pygments style dictionary. - (One that maps Token objects to style strings.) - """ - pygments_style = [] - - for token, style in pygments_dict.items(): - pygments_style.append((pygments_token_to_classname(token), style)) - - return Style(pygments_style) - - -def pygments_token_to_classname(token: "Token") -> str: - """ - Turn e.g. `Token.Name.Exception` into `'pygments.name.exception'`. - - (Our Pygments lexer will also turn the tokens that pygments produces in a - prompt_toolkit list of fragments that match these styling rules.) - """ - parts = ("pygments",) + token - return ".".join(parts).lower() +""" +Adaptor for building prompt_toolkit styles, starting from a Pygments style. + +Usage:: + + from pygments.styles.tango import TangoStyle + style = style_from_pygments_cls(pygments_style_cls=TangoStyle) +""" +from typing import TYPE_CHECKING, Dict, Type + +from .style import Style + +if TYPE_CHECKING: + from pygments.style import Style as PygmentsStyle + from pygments.token import Token + + +__all__ = [ + "style_from_pygments_cls", + "style_from_pygments_dict", + "pygments_token_to_classname", +] + + +def style_from_pygments_cls(pygments_style_cls: Type["PygmentsStyle"]) -> Style: + """ + Shortcut to create a :class:`.Style` instance from a Pygments style class + and a style dictionary. + + Example:: + + from prompt_toolkit.styles.from_pygments import style_from_pygments_cls + from pygments.styles import get_style_by_name + style = style_from_pygments_cls(get_style_by_name('monokai')) + + :param pygments_style_cls: Pygments style class to start from. + """ + # Import inline. + from pygments.style import Style as PygmentsStyle + + assert issubclass(pygments_style_cls, PygmentsStyle) + + return style_from_pygments_dict(pygments_style_cls.styles) + + +def style_from_pygments_dict(pygments_dict: Dict["Token", str]) -> Style: + """ + Create a :class:`.Style` instance from a Pygments style dictionary. + (One that maps Token objects to style strings.) + """ + pygments_style = [] + + for token, style in pygments_dict.items(): + pygments_style.append((pygments_token_to_classname(token), style)) + + return Style(pygments_style) + + +def pygments_token_to_classname(token: "Token") -> str: + """ + Turn e.g. `Token.Name.Exception` into `'pygments.name.exception'`. + + (Our Pygments lexer will also turn the tokens that pygments produces in a + prompt_toolkit list of fragments that match these styling rules.) + """ + parts = ("pygments",) + token + return ".".join(parts).lower() diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/style.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/style.py index 8b1eb78745..1474360fd2 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/style.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/style.py @@ -1,404 +1,404 @@ -""" -Tool for creating styles from a dictionary. -""" -import itertools -import re -import sys -from enum import Enum -from typing import Dict, Hashable, List, Set, Tuple, TypeVar - -from prompt_toolkit.cache import SimpleCache - -from .base import ( - ANSI_COLOR_NAMES, - ANSI_COLOR_NAMES_ALIASES, - DEFAULT_ATTRS, - Attrs, - BaseStyle, -) -from .named_colors import NAMED_COLORS - -__all__ = [ - "Style", - "parse_color", - "Priority", - "merge_styles", -] - -_named_colors_lowercase = {k.lower(): v.lstrip("#") for k, v in NAMED_COLORS.items()} - - -def parse_color(text: str) -> str: - """ - Parse/validate color format. - - Like in Pygments, but also support the ANSI color names. - (These will map to the colors of the 16 color palette.) - """ - # ANSI color names. - if text in ANSI_COLOR_NAMES: - return text - if text in ANSI_COLOR_NAMES_ALIASES: - return ANSI_COLOR_NAMES_ALIASES[text] - - # 140 named colors. - try: - # Replace by 'hex' value. - return _named_colors_lowercase[text.lower()] - except KeyError: - pass - - # Hex codes. - if text[0:1] == "#": - col = text[1:] - - # Keep this for backwards-compatibility (Pygments does it). - # I don't like the '#' prefix for named colors. - if col in ANSI_COLOR_NAMES: - return col - elif col in ANSI_COLOR_NAMES_ALIASES: - return ANSI_COLOR_NAMES_ALIASES[col] - - # 6 digit hex color. - elif len(col) == 6: - return col - - # 3 digit hex color. - elif len(col) == 3: - return col[0] * 2 + col[1] * 2 + col[2] * 2 - - # Default. - elif text in ("", "default"): - return text - - raise ValueError("Wrong color format %r" % text) - - -# Attributes, when they are not filled in by a style. None means that we take -# the value from the parent. -_EMPTY_ATTRS = Attrs( - color=None, - bgcolor=None, - bold=None, - underline=None, - strike=None, - italic=None, - blink=None, - reverse=None, - hidden=None, -) - - -def _expand_classname(classname: str) -> List[str]: - """ - Split a single class name at the `.` operator, and build a list of classes. - - E.g. 'a.b.c' becomes ['a', 'a.b', 'a.b.c'] - """ - result = [] - parts = classname.split(".") - - for i in range(1, len(parts) + 1): - result.append(".".join(parts[:i]).lower()) - - return result - - -def _parse_style_str(style_str: str) -> Attrs: - """ - Take a style string, e.g. 'bg:red #88ff00 class:title' - and return a `Attrs` instance. - """ - # Start from default Attrs. - if "noinherit" in style_str: - attrs = DEFAULT_ATTRS - else: - attrs = _EMPTY_ATTRS - - # Now update with the given attributes. - for part in style_str.split(): - if part == "noinherit": - pass - elif part == "bold": - attrs = attrs._replace(bold=True) - elif part == "nobold": - attrs = attrs._replace(bold=False) - elif part == "italic": - attrs = attrs._replace(italic=True) - elif part == "noitalic": - attrs = attrs._replace(italic=False) - elif part == "underline": - attrs = attrs._replace(underline=True) - elif part == "nounderline": - attrs = attrs._replace(underline=False) - elif part == "strike": - attrs = attrs._replace(strike=True) - elif part == "nostrike": - attrs = attrs._replace(strike=False) - - # prompt_toolkit extensions. Not in Pygments. - elif part == "blink": - attrs = attrs._replace(blink=True) - elif part == "noblink": - attrs = attrs._replace(blink=False) - elif part == "reverse": - attrs = attrs._replace(reverse=True) - elif part == "noreverse": - attrs = attrs._replace(reverse=False) - elif part == "hidden": - attrs = attrs._replace(hidden=True) - elif part == "nohidden": - attrs = attrs._replace(hidden=False) - - # Pygments properties that we ignore. - elif part in ("roman", "sans", "mono"): - pass - elif part.startswith("border:"): - pass - - # Ignore pieces in between square brackets. This is internal stuff. - # Like '[transparent]' or '[set-cursor-position]'. - elif part.startswith("[") and part.endswith("]"): - pass - - # Colors. - elif part.startswith("bg:"): - attrs = attrs._replace(bgcolor=parse_color(part[3:])) - elif part.startswith("fg:"): # The 'fg:' prefix is optional. - attrs = attrs._replace(color=parse_color(part[3:])) - else: - attrs = attrs._replace(color=parse_color(part)) - - return attrs - - -CLASS_NAMES_RE = re.compile(r"^[a-z0-9.\s_-]*$") # This one can't contain a comma! - - -class Priority(Enum): - """ - The priority of the rules, when a style is created from a dictionary. - - In a `Style`, rules that are defined later will always override previous - defined rules, however in a dictionary, the key order was arbitrary before - Python 3.6. This means that the style could change at random between rules. - - We have two options: - - - `DICT_KEY_ORDER`: This means, iterate through the dictionary, and take - the key/value pairs in order as they come. This is a good option if you - have Python >3.6. Rules at the end will override rules at the beginning. - - `MOST_PRECISE`: keys that are defined with most precision will get higher - priority. (More precise means: more elements.) - """ - - DICT_KEY_ORDER = "KEY_ORDER" - MOST_PRECISE = "MOST_PRECISE" - - -# In the latest python verions, we take the dictionary ordering like it is, -# In older versions, we sort by by precision. If you need to write code that -# runs on all Python versions, it's best to sort them manually, with the most -# precise rules at the bottom. -if sys.version_info >= (3, 6): - default_priority = Priority.DICT_KEY_ORDER -else: - default_priority = Priority.MOST_PRECISE - - -class Style(BaseStyle): - """ - Create a ``Style`` instance from a list of style rules. - - The `style_rules` is supposed to be a list of ('classnames', 'style') tuples. - The classnames are a whitespace separated string of class names and the - style string is just like a Pygments style definition, but with a few - additions: it supports 'reverse' and 'blink'. - - Later rules always override previous rules. - - Usage:: - - Style([ - ('title', '#ff0000 bold underline'), - ('something-else', 'reverse'), - ('class1 class2', 'reverse'), - ]) - - The ``from_dict`` classmethod is similar, but takes a dictionary as input. - """ - - def __init__(self, style_rules: List[Tuple[str, str]]) -> None: - class_names_and_attrs = [] - - # Loop through the rules in the order they were defined. - # Rules that are defined later get priority. - for class_names, style_str in style_rules: - assert CLASS_NAMES_RE.match(class_names), repr(class_names) - - # The order of the class names doesn't matter. - # (But the order of rules does matter.) - class_names_set = frozenset(class_names.lower().split()) - attrs = _parse_style_str(style_str) - - class_names_and_attrs.append((class_names_set, attrs)) - - self._style_rules = style_rules - self.class_names_and_attrs = class_names_and_attrs - - @property - def style_rules(self) -> List[Tuple[str, str]]: - return self._style_rules - - @classmethod - def from_dict( - cls, style_dict: Dict[str, str], priority: Priority = default_priority - ) -> "Style": - """ - :param style_dict: Style dictionary. - :param priority: `Priority` value. - """ - if priority == Priority.MOST_PRECISE: - - def key(item: Tuple[str, str]) -> int: - # Split on '.' and whitespace. Count elements. - return sum(len(i.split(".")) for i in item[0].split()) - - return cls(sorted(style_dict.items(), key=key)) - else: - return cls(list(style_dict.items())) - - def get_attrs_for_style_str( - self, style_str: str, default: Attrs = DEFAULT_ATTRS - ) -> Attrs: - """ - Get `Attrs` for the given style string. - """ - list_of_attrs = [default] - class_names: Set[str] = set() - - # Apply default styling. - for names, attr in self.class_names_and_attrs: - if not names: - list_of_attrs.append(attr) - - # Go from left to right through the style string. Things on the right - # take precedence. - for part in style_str.split(): - # This part represents a class. - # Do lookup of this class name in the style definition, as well - # as all class combinations that we have so far. - if part.startswith("class:"): - # Expand all class names (comma separated list). - new_class_names = [] - for p in part[6:].lower().split(","): - new_class_names.extend(_expand_classname(p)) - - for new_name in new_class_names: - # Build a set of all possible class combinations to be applied. - combos = set() - combos.add(frozenset([new_name])) - - for count in range(1, len(class_names) + 1): - for c2 in itertools.combinations(class_names, count): - combos.add(frozenset(c2 + (new_name,))) - - # Apply the styles that match these class names. - for names, attr in self.class_names_and_attrs: - if names in combos: - list_of_attrs.append(attr) - - class_names.add(new_name) - - # Process inline style. - else: - inline_attrs = _parse_style_str(part) - list_of_attrs.append(inline_attrs) - - return _merge_attrs(list_of_attrs) - - def invalidation_hash(self) -> Hashable: - return id(self.class_names_and_attrs) - - -_T = TypeVar("_T") - - -def _merge_attrs(list_of_attrs: List[Attrs]) -> Attrs: - """ - Take a list of :class:`.Attrs` instances and merge them into one. - Every `Attr` in the list can override the styling of the previous one. So, - the last one has highest priority. - """ - - def _or(*values: _T) -> _T: - "Take first not-None value, starting at the end." - for v in values[::-1]: - if v is not None: - return v - raise ValueError # Should not happen, there's always one non-null value. - - return Attrs( - color=_or("", *[a.color for a in list_of_attrs]), - bgcolor=_or("", *[a.bgcolor for a in list_of_attrs]), - bold=_or(False, *[a.bold for a in list_of_attrs]), - underline=_or(False, *[a.underline for a in list_of_attrs]), - strike=_or(False, *[a.strike for a in list_of_attrs]), - italic=_or(False, *[a.italic for a in list_of_attrs]), - blink=_or(False, *[a.blink for a in list_of_attrs]), - reverse=_or(False, *[a.reverse for a in list_of_attrs]), - hidden=_or(False, *[a.hidden for a in list_of_attrs]), - ) - - -def merge_styles(styles: List[BaseStyle]) -> "_MergedStyle": - """ - Merge multiple `Style` objects. - """ - styles = [s for s in styles if s is not None] - return _MergedStyle(styles) - - -class _MergedStyle(BaseStyle): - """ - Merge multiple `Style` objects into one. - This is supposed to ensure consistency: if any of the given styles changes, - then this style will be updated. - """ - - # NOTE: previously, we used an algorithm where we did not generate the - # combined style. Instead this was a proxy that called one style - # after the other, passing the outcome of the previous style as the - # default for the next one. This did not work, because that way, the - # priorities like described in the `Style` class don't work. - # 'class:aborted' was for instance never displayed in gray, because - # the next style specified a default color for any text. (The - # explicit styling of class:aborted should have taken priority, - # because it was more precise.) - def __init__(self, styles: List[BaseStyle]) -> None: - self.styles = styles - self._style: SimpleCache[Hashable, Style] = SimpleCache(maxsize=1) - - @property - def _merged_style(self) -> Style: - "The `Style` object that has the other styles merged together." - - def get() -> Style: - return Style(self.style_rules) - - return self._style.get(self.invalidation_hash(), get) - - @property - def style_rules(self) -> List[Tuple[str, str]]: - style_rules = [] - for s in self.styles: - style_rules.extend(s.style_rules) - return style_rules - - def get_attrs_for_style_str( - self, style_str: str, default: Attrs = DEFAULT_ATTRS - ) -> Attrs: - return self._merged_style.get_attrs_for_style_str(style_str, default) - - def invalidation_hash(self) -> Hashable: - return tuple(s.invalidation_hash() for s in self.styles) +""" +Tool for creating styles from a dictionary. +""" +import itertools +import re +import sys +from enum import Enum +from typing import Dict, Hashable, List, Set, Tuple, TypeVar + +from prompt_toolkit.cache import SimpleCache + +from .base import ( + ANSI_COLOR_NAMES, + ANSI_COLOR_NAMES_ALIASES, + DEFAULT_ATTRS, + Attrs, + BaseStyle, +) +from .named_colors import NAMED_COLORS + +__all__ = [ + "Style", + "parse_color", + "Priority", + "merge_styles", +] + +_named_colors_lowercase = {k.lower(): v.lstrip("#") for k, v in NAMED_COLORS.items()} + + +def parse_color(text: str) -> str: + """ + Parse/validate color format. + + Like in Pygments, but also support the ANSI color names. + (These will map to the colors of the 16 color palette.) + """ + # ANSI color names. + if text in ANSI_COLOR_NAMES: + return text + if text in ANSI_COLOR_NAMES_ALIASES: + return ANSI_COLOR_NAMES_ALIASES[text] + + # 140 named colors. + try: + # Replace by 'hex' value. + return _named_colors_lowercase[text.lower()] + except KeyError: + pass + + # Hex codes. + if text[0:1] == "#": + col = text[1:] + + # Keep this for backwards-compatibility (Pygments does it). + # I don't like the '#' prefix for named colors. + if col in ANSI_COLOR_NAMES: + return col + elif col in ANSI_COLOR_NAMES_ALIASES: + return ANSI_COLOR_NAMES_ALIASES[col] + + # 6 digit hex color. + elif len(col) == 6: + return col + + # 3 digit hex color. + elif len(col) == 3: + return col[0] * 2 + col[1] * 2 + col[2] * 2 + + # Default. + elif text in ("", "default"): + return text + + raise ValueError("Wrong color format %r" % text) + + +# Attributes, when they are not filled in by a style. None means that we take +# the value from the parent. +_EMPTY_ATTRS = Attrs( + color=None, + bgcolor=None, + bold=None, + underline=None, + strike=None, + italic=None, + blink=None, + reverse=None, + hidden=None, +) + + +def _expand_classname(classname: str) -> List[str]: + """ + Split a single class name at the `.` operator, and build a list of classes. + + E.g. 'a.b.c' becomes ['a', 'a.b', 'a.b.c'] + """ + result = [] + parts = classname.split(".") + + for i in range(1, len(parts) + 1): + result.append(".".join(parts[:i]).lower()) + + return result + + +def _parse_style_str(style_str: str) -> Attrs: + """ + Take a style string, e.g. 'bg:red #88ff00 class:title' + and return a `Attrs` instance. + """ + # Start from default Attrs. + if "noinherit" in style_str: + attrs = DEFAULT_ATTRS + else: + attrs = _EMPTY_ATTRS + + # Now update with the given attributes. + for part in style_str.split(): + if part == "noinherit": + pass + elif part == "bold": + attrs = attrs._replace(bold=True) + elif part == "nobold": + attrs = attrs._replace(bold=False) + elif part == "italic": + attrs = attrs._replace(italic=True) + elif part == "noitalic": + attrs = attrs._replace(italic=False) + elif part == "underline": + attrs = attrs._replace(underline=True) + elif part == "nounderline": + attrs = attrs._replace(underline=False) + elif part == "strike": + attrs = attrs._replace(strike=True) + elif part == "nostrike": + attrs = attrs._replace(strike=False) + + # prompt_toolkit extensions. Not in Pygments. + elif part == "blink": + attrs = attrs._replace(blink=True) + elif part == "noblink": + attrs = attrs._replace(blink=False) + elif part == "reverse": + attrs = attrs._replace(reverse=True) + elif part == "noreverse": + attrs = attrs._replace(reverse=False) + elif part == "hidden": + attrs = attrs._replace(hidden=True) + elif part == "nohidden": + attrs = attrs._replace(hidden=False) + + # Pygments properties that we ignore. + elif part in ("roman", "sans", "mono"): + pass + elif part.startswith("border:"): + pass + + # Ignore pieces in between square brackets. This is internal stuff. + # Like '[transparent]' or '[set-cursor-position]'. + elif part.startswith("[") and part.endswith("]"): + pass + + # Colors. + elif part.startswith("bg:"): + attrs = attrs._replace(bgcolor=parse_color(part[3:])) + elif part.startswith("fg:"): # The 'fg:' prefix is optional. + attrs = attrs._replace(color=parse_color(part[3:])) + else: + attrs = attrs._replace(color=parse_color(part)) + + return attrs + + +CLASS_NAMES_RE = re.compile(r"^[a-z0-9.\s_-]*$") # This one can't contain a comma! + + +class Priority(Enum): + """ + The priority of the rules, when a style is created from a dictionary. + + In a `Style`, rules that are defined later will always override previous + defined rules, however in a dictionary, the key order was arbitrary before + Python 3.6. This means that the style could change at random between rules. + + We have two options: + + - `DICT_KEY_ORDER`: This means, iterate through the dictionary, and take + the key/value pairs in order as they come. This is a good option if you + have Python >3.6. Rules at the end will override rules at the beginning. + - `MOST_PRECISE`: keys that are defined with most precision will get higher + priority. (More precise means: more elements.) + """ + + DICT_KEY_ORDER = "KEY_ORDER" + MOST_PRECISE = "MOST_PRECISE" + + +# In the latest python verions, we take the dictionary ordering like it is, +# In older versions, we sort by by precision. If you need to write code that +# runs on all Python versions, it's best to sort them manually, with the most +# precise rules at the bottom. +if sys.version_info >= (3, 6): + default_priority = Priority.DICT_KEY_ORDER +else: + default_priority = Priority.MOST_PRECISE + + +class Style(BaseStyle): + """ + Create a ``Style`` instance from a list of style rules. + + The `style_rules` is supposed to be a list of ('classnames', 'style') tuples. + The classnames are a whitespace separated string of class names and the + style string is just like a Pygments style definition, but with a few + additions: it supports 'reverse' and 'blink'. + + Later rules always override previous rules. + + Usage:: + + Style([ + ('title', '#ff0000 bold underline'), + ('something-else', 'reverse'), + ('class1 class2', 'reverse'), + ]) + + The ``from_dict`` classmethod is similar, but takes a dictionary as input. + """ + + def __init__(self, style_rules: List[Tuple[str, str]]) -> None: + class_names_and_attrs = [] + + # Loop through the rules in the order they were defined. + # Rules that are defined later get priority. + for class_names, style_str in style_rules: + assert CLASS_NAMES_RE.match(class_names), repr(class_names) + + # The order of the class names doesn't matter. + # (But the order of rules does matter.) + class_names_set = frozenset(class_names.lower().split()) + attrs = _parse_style_str(style_str) + + class_names_and_attrs.append((class_names_set, attrs)) + + self._style_rules = style_rules + self.class_names_and_attrs = class_names_and_attrs + + @property + def style_rules(self) -> List[Tuple[str, str]]: + return self._style_rules + + @classmethod + def from_dict( + cls, style_dict: Dict[str, str], priority: Priority = default_priority + ) -> "Style": + """ + :param style_dict: Style dictionary. + :param priority: `Priority` value. + """ + if priority == Priority.MOST_PRECISE: + + def key(item: Tuple[str, str]) -> int: + # Split on '.' and whitespace. Count elements. + return sum(len(i.split(".")) for i in item[0].split()) + + return cls(sorted(style_dict.items(), key=key)) + else: + return cls(list(style_dict.items())) + + def get_attrs_for_style_str( + self, style_str: str, default: Attrs = DEFAULT_ATTRS + ) -> Attrs: + """ + Get `Attrs` for the given style string. + """ + list_of_attrs = [default] + class_names: Set[str] = set() + + # Apply default styling. + for names, attr in self.class_names_and_attrs: + if not names: + list_of_attrs.append(attr) + + # Go from left to right through the style string. Things on the right + # take precedence. + for part in style_str.split(): + # This part represents a class. + # Do lookup of this class name in the style definition, as well + # as all class combinations that we have so far. + if part.startswith("class:"): + # Expand all class names (comma separated list). + new_class_names = [] + for p in part[6:].lower().split(","): + new_class_names.extend(_expand_classname(p)) + + for new_name in new_class_names: + # Build a set of all possible class combinations to be applied. + combos = set() + combos.add(frozenset([new_name])) + + for count in range(1, len(class_names) + 1): + for c2 in itertools.combinations(class_names, count): + combos.add(frozenset(c2 + (new_name,))) + + # Apply the styles that match these class names. + for names, attr in self.class_names_and_attrs: + if names in combos: + list_of_attrs.append(attr) + + class_names.add(new_name) + + # Process inline style. + else: + inline_attrs = _parse_style_str(part) + list_of_attrs.append(inline_attrs) + + return _merge_attrs(list_of_attrs) + + def invalidation_hash(self) -> Hashable: + return id(self.class_names_and_attrs) + + +_T = TypeVar("_T") + + +def _merge_attrs(list_of_attrs: List[Attrs]) -> Attrs: + """ + Take a list of :class:`.Attrs` instances and merge them into one. + Every `Attr` in the list can override the styling of the previous one. So, + the last one has highest priority. + """ + + def _or(*values: _T) -> _T: + "Take first not-None value, starting at the end." + for v in values[::-1]: + if v is not None: + return v + raise ValueError # Should not happen, there's always one non-null value. + + return Attrs( + color=_or("", *[a.color for a in list_of_attrs]), + bgcolor=_or("", *[a.bgcolor for a in list_of_attrs]), + bold=_or(False, *[a.bold for a in list_of_attrs]), + underline=_or(False, *[a.underline for a in list_of_attrs]), + strike=_or(False, *[a.strike for a in list_of_attrs]), + italic=_or(False, *[a.italic for a in list_of_attrs]), + blink=_or(False, *[a.blink for a in list_of_attrs]), + reverse=_or(False, *[a.reverse for a in list_of_attrs]), + hidden=_or(False, *[a.hidden for a in list_of_attrs]), + ) + + +def merge_styles(styles: List[BaseStyle]) -> "_MergedStyle": + """ + Merge multiple `Style` objects. + """ + styles = [s for s in styles if s is not None] + return _MergedStyle(styles) + + +class _MergedStyle(BaseStyle): + """ + Merge multiple `Style` objects into one. + This is supposed to ensure consistency: if any of the given styles changes, + then this style will be updated. + """ + + # NOTE: previously, we used an algorithm where we did not generate the + # combined style. Instead this was a proxy that called one style + # after the other, passing the outcome of the previous style as the + # default for the next one. This did not work, because that way, the + # priorities like described in the `Style` class don't work. + # 'class:aborted' was for instance never displayed in gray, because + # the next style specified a default color for any text. (The + # explicit styling of class:aborted should have taken priority, + # because it was more precise.) + def __init__(self, styles: List[BaseStyle]) -> None: + self.styles = styles + self._style: SimpleCache[Hashable, Style] = SimpleCache(maxsize=1) + + @property + def _merged_style(self) -> Style: + "The `Style` object that has the other styles merged together." + + def get() -> Style: + return Style(self.style_rules) + + return self._style.get(self.invalidation_hash(), get) + + @property + def style_rules(self) -> List[Tuple[str, str]]: + style_rules = [] + for s in self.styles: + style_rules.extend(s.style_rules) + return style_rules + + def get_attrs_for_style_str( + self, style_str: str, default: Attrs = DEFAULT_ATTRS + ) -> Attrs: + return self._merged_style.get_attrs_for_style_str(style_str, default) + + def invalidation_hash(self) -> Hashable: + return tuple(s.invalidation_hash() for s in self.styles) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/style_transformation.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/style_transformation.py index c91904de57..15b858aa9f 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/style_transformation.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/styles/style_transformation.py @@ -1,375 +1,375 @@ -""" -Collection of style transformations. - -Think of it as a kind of color post processing after the rendering is done. -This could be used for instance to change the contrast/saturation; swap light -and dark colors or even change certain colors for other colors. - -When the UI is rendered, these transformations can be applied right after the -style strings are turned into `Attrs` objects that represent the actual -formatting. -""" -from abc import ABCMeta, abstractmethod -from colorsys import hls_to_rgb, rgb_to_hls -from typing import Callable, Hashable, Optional, Sequence, Tuple, Union - -from prompt_toolkit.cache import memoized -from prompt_toolkit.filters import FilterOrBool, to_filter -from prompt_toolkit.utils import AnyFloat, to_float, to_str - -from .base import ANSI_COLOR_NAMES, Attrs -from .style import parse_color - -__all__ = [ - "StyleTransformation", - "SwapLightAndDarkStyleTransformation", - "ReverseStyleTransformation", - "SetDefaultColorStyleTransformation", - "AdjustBrightnessStyleTransformation", - "DummyStyleTransformation", - "ConditionalStyleTransformation", - "DynamicStyleTransformation", - "merge_style_transformations", -] - - -class StyleTransformation(metaclass=ABCMeta): - """ - Base class for any style transformation. - """ - - @abstractmethod - def transform_attrs(self, attrs: Attrs) -> Attrs: - """ - Take an `Attrs` object and return a new `Attrs` object. - - Remember that the color formats can be either "ansi..." or a 6 digit - lowercase hexadecimal color (without '#' prefix). - """ - - def invalidation_hash(self) -> Hashable: - """ - When this changes, the cache should be invalidated. - """ - return "%s-%s" % (self.__class__.__name__, id(self)) - - -class SwapLightAndDarkStyleTransformation(StyleTransformation): - """ - Turn dark colors into light colors and the other way around. - - This is meant to make color schemes that work on a dark background usable - on a light background (and the other way around). - - Notice that this doesn't swap foreground and background like "reverse" - does. It turns light green into dark green and the other way around. - Foreground and background colors are considered individually. - - Also notice that when <reverse> is used somewhere and no colors are given - in particular (like what is the default for the bottom toolbar), then this - doesn't change anything. This is what makes sense, because when the - 'default' color is chosen, it's what works best for the terminal, and - reverse works good with that. - """ - - def transform_attrs(self, attrs: Attrs) -> Attrs: - """ - Return the `Attrs` used when opposite luminosity should be used. - """ - # Reverse colors. - attrs = attrs._replace(color=get_opposite_color(attrs.color)) - attrs = attrs._replace(bgcolor=get_opposite_color(attrs.bgcolor)) - - return attrs - - -class ReverseStyleTransformation(StyleTransformation): - """ - Swap the 'reverse' attribute. - - (This is still experimental.) - """ - - def transform_attrs(self, attrs: Attrs) -> Attrs: - return attrs._replace(reverse=not attrs.reverse) - - -class SetDefaultColorStyleTransformation(StyleTransformation): - """ - Set default foreground/background color for output that doesn't specify - anything. This is useful for overriding the terminal default colors. - - :param fg: Color string or callable that returns a color string for the - foreground. - :param bg: Like `fg`, but for the background. - """ - - def __init__( - self, fg: Union[str, Callable[[], str]], bg: Union[str, Callable[[], str]] - ) -> None: - - self.fg = fg - self.bg = bg - - def transform_attrs(self, attrs: Attrs) -> Attrs: - if attrs.bgcolor in ("", "default"): - attrs = attrs._replace(bgcolor=parse_color(to_str(self.bg))) - - if attrs.color in ("", "default"): - attrs = attrs._replace(color=parse_color(to_str(self.fg))) - - return attrs - - def invalidation_hash(self) -> Hashable: - return ( - "set-default-color", - to_str(self.fg), - to_str(self.bg), - ) - - -class AdjustBrightnessStyleTransformation(StyleTransformation): - """ - Adjust the brightness to improve the rendering on either dark or light - backgrounds. - - For dark backgrounds, it's best to increase `min_brightness`. For light - backgrounds it's best to decrease `max_brightness`. Usually, only one - setting is adjusted. - - This will only change the brightness for text that has a foreground color - defined, but no background color. It works best for 256 or true color - output. - - .. note:: Notice that there is no universal way to detect whether the - application is running in a light or dark terminal. As a - developer of an command line application, you'll have to make - this configurable for the user. - - :param min_brightness: Float between 0.0 and 1.0 or a callable that returns - a float. - :param max_brightness: Float between 0.0 and 1.0 or a callable that returns - a float. - """ - - def __init__( - self, min_brightness: AnyFloat = 0.0, max_brightness: AnyFloat = 1.0 - ) -> None: - - self.min_brightness = min_brightness - self.max_brightness = max_brightness - - def transform_attrs(self, attrs: Attrs) -> Attrs: - min_brightness = to_float(self.min_brightness) - max_brightness = to_float(self.max_brightness) - assert 0 <= min_brightness <= 1 - assert 0 <= max_brightness <= 1 - - # Don't do anything if the whole brightness range is acceptable. - # This also avoids turning ansi colors into RGB sequences. - if min_brightness == 0.0 and max_brightness == 1.0: - return attrs - - # If a foreground color is given without a background color. - no_background = not attrs.bgcolor or attrs.bgcolor == "default" - has_fgcolor = attrs.color and attrs.color != "ansidefault" - - if has_fgcolor and no_background: - # Calculate new RGB values. - r, g, b = self._color_to_rgb(attrs.color or "") - hue, brightness, saturation = rgb_to_hls(r, g, b) - brightness = self._interpolate_brightness( - brightness, min_brightness, max_brightness - ) - r, g, b = hls_to_rgb(hue, brightness, saturation) - new_color = "%02x%02x%02x" % (int(r * 255), int(g * 255), int(b * 255)) - - attrs = attrs._replace(color=new_color) - - return attrs - - def _color_to_rgb(self, color: str) -> Tuple[float, float, float]: - """ - Parse `style.Attrs` color into RGB tuple. - """ - # Do RGB lookup for ANSI colors. - try: - from prompt_toolkit.output.vt100 import ANSI_COLORS_TO_RGB - - r, g, b = ANSI_COLORS_TO_RGB[color] - return r / 255.0, g / 255.0, b / 255.0 - except KeyError: - pass - - # Parse RRGGBB format. - return ( - int(color[0:2], 16) / 255.0, - int(color[2:4], 16) / 255.0, - int(color[4:6], 16) / 255.0, - ) - - # NOTE: we don't have to support named colors here. They are already - # transformed into RGB values in `style.parse_color`. - - def _interpolate_brightness( - self, value: float, min_brightness: float, max_brightness: float - ) -> float: - """ - Map the brightness to the (min_brightness..max_brightness) range. - """ - return min_brightness + (max_brightness - min_brightness) * value - - def invalidation_hash(self) -> Hashable: - return ( - "adjust-brightness", - to_float(self.min_brightness), - to_float(self.max_brightness), - ) - - -class DummyStyleTransformation(StyleTransformation): - """ - Don't transform anything at all. - """ - - def transform_attrs(self, attrs: Attrs) -> Attrs: - return attrs - - def invalidation_hash(self) -> Hashable: - # Always return the same hash for these dummy instances. - return "dummy-style-transformation" - - -class DynamicStyleTransformation(StyleTransformation): - """ - StyleTransformation class that can dynamically returns any - `StyleTransformation`. - - :param get_style_transformation: Callable that returns a - :class:`.StyleTransformation` instance. - """ - - def __init__( - self, get_style_transformation: Callable[[], Optional[StyleTransformation]] - ) -> None: - - self.get_style_transformation = get_style_transformation - - def transform_attrs(self, attrs: Attrs) -> Attrs: - style_transformation = ( - self.get_style_transformation() or DummyStyleTransformation() - ) - return style_transformation.transform_attrs(attrs) - - def invalidation_hash(self) -> Hashable: - style_transformation = ( - self.get_style_transformation() or DummyStyleTransformation() - ) - return style_transformation.invalidation_hash() - - -class ConditionalStyleTransformation(StyleTransformation): - """ - Apply the style transformation depending on a condition. - """ - - def __init__( - self, style_transformation: StyleTransformation, filter: FilterOrBool - ) -> None: - - self.style_transformation = style_transformation - self.filter = to_filter(filter) - - def transform_attrs(self, attrs: Attrs) -> Attrs: - if self.filter(): - return self.style_transformation.transform_attrs(attrs) - return attrs - - def invalidation_hash(self) -> Hashable: - return (self.filter(), self.style_transformation.invalidation_hash()) - - -class _MergedStyleTransformation(StyleTransformation): - def __init__(self, style_transformations: Sequence[StyleTransformation]) -> None: - self.style_transformations = style_transformations - - def transform_attrs(self, attrs: Attrs) -> Attrs: - for transformation in self.style_transformations: - attrs = transformation.transform_attrs(attrs) - return attrs - - def invalidation_hash(self) -> Hashable: - return tuple(t.invalidation_hash() for t in self.style_transformations) - - -def merge_style_transformations( - style_transformations: Sequence[StyleTransformation], -) -> StyleTransformation: - """ - Merge multiple transformations together. - """ - return _MergedStyleTransformation(style_transformations) - - -# Dictionary that maps ANSI color names to their opposite. This is useful for -# turning color schemes that are optimized for a black background usable for a -# white background. -OPPOSITE_ANSI_COLOR_NAMES = { - "ansidefault": "ansidefault", - "ansiblack": "ansiwhite", - "ansired": "ansibrightred", - "ansigreen": "ansibrightgreen", - "ansiyellow": "ansibrightyellow", - "ansiblue": "ansibrightblue", - "ansimagenta": "ansibrightmagenta", - "ansicyan": "ansibrightcyan", - "ansigray": "ansibrightblack", - "ansiwhite": "ansiblack", - "ansibrightred": "ansired", - "ansibrightgreen": "ansigreen", - "ansibrightyellow": "ansiyellow", - "ansibrightblue": "ansiblue", - "ansibrightmagenta": "ansimagenta", - "ansibrightcyan": "ansicyan", - "ansibrightblack": "ansigray", -} -assert set(OPPOSITE_ANSI_COLOR_NAMES.keys()) == set(ANSI_COLOR_NAMES) -assert set(OPPOSITE_ANSI_COLOR_NAMES.values()) == set(ANSI_COLOR_NAMES) - - -@memoized() -def get_opposite_color(colorname: Optional[str]) -> Optional[str]: - """ - Take a color name in either 'ansi...' format or 6 digit RGB, return the - color of opposite luminosity (same hue/saturation). - - This is used for turning color schemes that work on a light background - usable on a dark background. - """ - if colorname is None: # Because color/bgcolor can be None in `Attrs`. - return None - - # Special values. - if colorname in ("", "default"): - return colorname - - # Try ANSI color names. - try: - return OPPOSITE_ANSI_COLOR_NAMES[colorname] - except KeyError: - # Try 6 digit RGB colors. - r = int(colorname[:2], 16) / 255.0 - g = int(colorname[2:4], 16) / 255.0 - b = int(colorname[4:6], 16) / 255.0 - - h, l, s = rgb_to_hls(r, g, b) - - l = 1 - l - - r, g, b = hls_to_rgb(h, l, s) - - r = int(r * 255) - g = int(g * 255) - b = int(b * 255) - - return "%02x%02x%02x" % (r, g, b) +""" +Collection of style transformations. + +Think of it as a kind of color post processing after the rendering is done. +This could be used for instance to change the contrast/saturation; swap light +and dark colors or even change certain colors for other colors. + +When the UI is rendered, these transformations can be applied right after the +style strings are turned into `Attrs` objects that represent the actual +formatting. +""" +from abc import ABCMeta, abstractmethod +from colorsys import hls_to_rgb, rgb_to_hls +from typing import Callable, Hashable, Optional, Sequence, Tuple, Union + +from prompt_toolkit.cache import memoized +from prompt_toolkit.filters import FilterOrBool, to_filter +from prompt_toolkit.utils import AnyFloat, to_float, to_str + +from .base import ANSI_COLOR_NAMES, Attrs +from .style import parse_color + +__all__ = [ + "StyleTransformation", + "SwapLightAndDarkStyleTransformation", + "ReverseStyleTransformation", + "SetDefaultColorStyleTransformation", + "AdjustBrightnessStyleTransformation", + "DummyStyleTransformation", + "ConditionalStyleTransformation", + "DynamicStyleTransformation", + "merge_style_transformations", +] + + +class StyleTransformation(metaclass=ABCMeta): + """ + Base class for any style transformation. + """ + + @abstractmethod + def transform_attrs(self, attrs: Attrs) -> Attrs: + """ + Take an `Attrs` object and return a new `Attrs` object. + + Remember that the color formats can be either "ansi..." or a 6 digit + lowercase hexadecimal color (without '#' prefix). + """ + + def invalidation_hash(self) -> Hashable: + """ + When this changes, the cache should be invalidated. + """ + return "%s-%s" % (self.__class__.__name__, id(self)) + + +class SwapLightAndDarkStyleTransformation(StyleTransformation): + """ + Turn dark colors into light colors and the other way around. + + This is meant to make color schemes that work on a dark background usable + on a light background (and the other way around). + + Notice that this doesn't swap foreground and background like "reverse" + does. It turns light green into dark green and the other way around. + Foreground and background colors are considered individually. + + Also notice that when <reverse> is used somewhere and no colors are given + in particular (like what is the default for the bottom toolbar), then this + doesn't change anything. This is what makes sense, because when the + 'default' color is chosen, it's what works best for the terminal, and + reverse works good with that. + """ + + def transform_attrs(self, attrs: Attrs) -> Attrs: + """ + Return the `Attrs` used when opposite luminosity should be used. + """ + # Reverse colors. + attrs = attrs._replace(color=get_opposite_color(attrs.color)) + attrs = attrs._replace(bgcolor=get_opposite_color(attrs.bgcolor)) + + return attrs + + +class ReverseStyleTransformation(StyleTransformation): + """ + Swap the 'reverse' attribute. + + (This is still experimental.) + """ + + def transform_attrs(self, attrs: Attrs) -> Attrs: + return attrs._replace(reverse=not attrs.reverse) + + +class SetDefaultColorStyleTransformation(StyleTransformation): + """ + Set default foreground/background color for output that doesn't specify + anything. This is useful for overriding the terminal default colors. + + :param fg: Color string or callable that returns a color string for the + foreground. + :param bg: Like `fg`, but for the background. + """ + + def __init__( + self, fg: Union[str, Callable[[], str]], bg: Union[str, Callable[[], str]] + ) -> None: + + self.fg = fg + self.bg = bg + + def transform_attrs(self, attrs: Attrs) -> Attrs: + if attrs.bgcolor in ("", "default"): + attrs = attrs._replace(bgcolor=parse_color(to_str(self.bg))) + + if attrs.color in ("", "default"): + attrs = attrs._replace(color=parse_color(to_str(self.fg))) + + return attrs + + def invalidation_hash(self) -> Hashable: + return ( + "set-default-color", + to_str(self.fg), + to_str(self.bg), + ) + + +class AdjustBrightnessStyleTransformation(StyleTransformation): + """ + Adjust the brightness to improve the rendering on either dark or light + backgrounds. + + For dark backgrounds, it's best to increase `min_brightness`. For light + backgrounds it's best to decrease `max_brightness`. Usually, only one + setting is adjusted. + + This will only change the brightness for text that has a foreground color + defined, but no background color. It works best for 256 or true color + output. + + .. note:: Notice that there is no universal way to detect whether the + application is running in a light or dark terminal. As a + developer of an command line application, you'll have to make + this configurable for the user. + + :param min_brightness: Float between 0.0 and 1.0 or a callable that returns + a float. + :param max_brightness: Float between 0.0 and 1.0 or a callable that returns + a float. + """ + + def __init__( + self, min_brightness: AnyFloat = 0.0, max_brightness: AnyFloat = 1.0 + ) -> None: + + self.min_brightness = min_brightness + self.max_brightness = max_brightness + + def transform_attrs(self, attrs: Attrs) -> Attrs: + min_brightness = to_float(self.min_brightness) + max_brightness = to_float(self.max_brightness) + assert 0 <= min_brightness <= 1 + assert 0 <= max_brightness <= 1 + + # Don't do anything if the whole brightness range is acceptable. + # This also avoids turning ansi colors into RGB sequences. + if min_brightness == 0.0 and max_brightness == 1.0: + return attrs + + # If a foreground color is given without a background color. + no_background = not attrs.bgcolor or attrs.bgcolor == "default" + has_fgcolor = attrs.color and attrs.color != "ansidefault" + + if has_fgcolor and no_background: + # Calculate new RGB values. + r, g, b = self._color_to_rgb(attrs.color or "") + hue, brightness, saturation = rgb_to_hls(r, g, b) + brightness = self._interpolate_brightness( + brightness, min_brightness, max_brightness + ) + r, g, b = hls_to_rgb(hue, brightness, saturation) + new_color = "%02x%02x%02x" % (int(r * 255), int(g * 255), int(b * 255)) + + attrs = attrs._replace(color=new_color) + + return attrs + + def _color_to_rgb(self, color: str) -> Tuple[float, float, float]: + """ + Parse `style.Attrs` color into RGB tuple. + """ + # Do RGB lookup for ANSI colors. + try: + from prompt_toolkit.output.vt100 import ANSI_COLORS_TO_RGB + + r, g, b = ANSI_COLORS_TO_RGB[color] + return r / 255.0, g / 255.0, b / 255.0 + except KeyError: + pass + + # Parse RRGGBB format. + return ( + int(color[0:2], 16) / 255.0, + int(color[2:4], 16) / 255.0, + int(color[4:6], 16) / 255.0, + ) + + # NOTE: we don't have to support named colors here. They are already + # transformed into RGB values in `style.parse_color`. + + def _interpolate_brightness( + self, value: float, min_brightness: float, max_brightness: float + ) -> float: + """ + Map the brightness to the (min_brightness..max_brightness) range. + """ + return min_brightness + (max_brightness - min_brightness) * value + + def invalidation_hash(self) -> Hashable: + return ( + "adjust-brightness", + to_float(self.min_brightness), + to_float(self.max_brightness), + ) + + +class DummyStyleTransformation(StyleTransformation): + """ + Don't transform anything at all. + """ + + def transform_attrs(self, attrs: Attrs) -> Attrs: + return attrs + + def invalidation_hash(self) -> Hashable: + # Always return the same hash for these dummy instances. + return "dummy-style-transformation" + + +class DynamicStyleTransformation(StyleTransformation): + """ + StyleTransformation class that can dynamically returns any + `StyleTransformation`. + + :param get_style_transformation: Callable that returns a + :class:`.StyleTransformation` instance. + """ + + def __init__( + self, get_style_transformation: Callable[[], Optional[StyleTransformation]] + ) -> None: + + self.get_style_transformation = get_style_transformation + + def transform_attrs(self, attrs: Attrs) -> Attrs: + style_transformation = ( + self.get_style_transformation() or DummyStyleTransformation() + ) + return style_transformation.transform_attrs(attrs) + + def invalidation_hash(self) -> Hashable: + style_transformation = ( + self.get_style_transformation() or DummyStyleTransformation() + ) + return style_transformation.invalidation_hash() + + +class ConditionalStyleTransformation(StyleTransformation): + """ + Apply the style transformation depending on a condition. + """ + + def __init__( + self, style_transformation: StyleTransformation, filter: FilterOrBool + ) -> None: + + self.style_transformation = style_transformation + self.filter = to_filter(filter) + + def transform_attrs(self, attrs: Attrs) -> Attrs: + if self.filter(): + return self.style_transformation.transform_attrs(attrs) + return attrs + + def invalidation_hash(self) -> Hashable: + return (self.filter(), self.style_transformation.invalidation_hash()) + + +class _MergedStyleTransformation(StyleTransformation): + def __init__(self, style_transformations: Sequence[StyleTransformation]) -> None: + self.style_transformations = style_transformations + + def transform_attrs(self, attrs: Attrs) -> Attrs: + for transformation in self.style_transformations: + attrs = transformation.transform_attrs(attrs) + return attrs + + def invalidation_hash(self) -> Hashable: + return tuple(t.invalidation_hash() for t in self.style_transformations) + + +def merge_style_transformations( + style_transformations: Sequence[StyleTransformation], +) -> StyleTransformation: + """ + Merge multiple transformations together. + """ + return _MergedStyleTransformation(style_transformations) + + +# Dictionary that maps ANSI color names to their opposite. This is useful for +# turning color schemes that are optimized for a black background usable for a +# white background. +OPPOSITE_ANSI_COLOR_NAMES = { + "ansidefault": "ansidefault", + "ansiblack": "ansiwhite", + "ansired": "ansibrightred", + "ansigreen": "ansibrightgreen", + "ansiyellow": "ansibrightyellow", + "ansiblue": "ansibrightblue", + "ansimagenta": "ansibrightmagenta", + "ansicyan": "ansibrightcyan", + "ansigray": "ansibrightblack", + "ansiwhite": "ansiblack", + "ansibrightred": "ansired", + "ansibrightgreen": "ansigreen", + "ansibrightyellow": "ansiyellow", + "ansibrightblue": "ansiblue", + "ansibrightmagenta": "ansimagenta", + "ansibrightcyan": "ansicyan", + "ansibrightblack": "ansigray", +} +assert set(OPPOSITE_ANSI_COLOR_NAMES.keys()) == set(ANSI_COLOR_NAMES) +assert set(OPPOSITE_ANSI_COLOR_NAMES.values()) == set(ANSI_COLOR_NAMES) + + +@memoized() +def get_opposite_color(colorname: Optional[str]) -> Optional[str]: + """ + Take a color name in either 'ansi...' format or 6 digit RGB, return the + color of opposite luminosity (same hue/saturation). + + This is used for turning color schemes that work on a light background + usable on a dark background. + """ + if colorname is None: # Because color/bgcolor can be None in `Attrs`. + return None + + # Special values. + if colorname in ("", "default"): + return colorname + + # Try ANSI color names. + try: + return OPPOSITE_ANSI_COLOR_NAMES[colorname] + except KeyError: + # Try 6 digit RGB colors. + r = int(colorname[:2], 16) / 255.0 + g = int(colorname[2:4], 16) / 255.0 + b = int(colorname[4:6], 16) / 255.0 + + h, l, s = rgb_to_hls(r, g, b) + + l = 1 - l + + r, g, b = hls_to_rgb(h, l, s) + + r = int(r * 255) + g = int(g * 255) + b = int(b * 255) + + return "%02x%02x%02x" % (r, g, b) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/token.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/token.py index 9da7d69805..76fd9c46cd 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/token.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/token.py @@ -1,8 +1,8 @@ -""" -""" - -__all__ = [ - "ZeroWidthEscape", -] - -ZeroWidthEscape = "[ZeroWidthEscape]" +""" +""" + +__all__ = [ + "ZeroWidthEscape", +] + +ZeroWidthEscape = "[ZeroWidthEscape]" diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/utils.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/utils.py index f7dab5301e..8b501e5b23 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/utils.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/utils.py @@ -1,325 +1,325 @@ -import os -import signal -import sys -import threading -from collections import deque -from typing import ( - Callable, - ContextManager, - Deque, - Dict, - Generator, - Generic, - List, - Optional, - TypeVar, - Union, -) - -from wcwidth import wcwidth - -__all__ = [ - "Event", - "DummyContext", - "get_cwidth", - "suspend_to_background_supported", - "is_conemu_ansi", - "is_windows", - "in_main_thread", - "get_bell_environment_variable", - "get_term_environment_variable", - "take_using_weights", - "to_str", - "to_int", - "AnyFloat", - "to_float", - "is_dumb_terminal", -] - -# Used to ensure sphinx autodoc does not try to import platform-specific -# stuff when documenting win32.py modules. -SPHINX_AUTODOC_RUNNING = "sphinx.ext.autodoc" in sys.modules - -_Sender = TypeVar("_Sender", covariant=True) - - -class Event(Generic[_Sender]): - """ - Simple event to which event handlers can be attached. For instance:: - - class Cls: - def __init__(self): - # Define event. The first parameter is the sender. - self.event = Event(self) - - obj = Cls() - - def handler(sender): - pass - - # Add event handler by using the += operator. - obj.event += handler - - # Fire event. - obj.event() - """ - - def __init__( - self, sender: _Sender, handler: Optional[Callable[[_Sender], None]] = None - ) -> None: - self.sender = sender - self._handlers: List[Callable[[_Sender], None]] = [] - - if handler is not None: - self += handler - - def __call__(self) -> None: - "Fire event." - for handler in self._handlers: - handler(self.sender) - - def fire(self) -> None: - "Alias for just calling the event." - self() - - def add_handler(self, handler: Callable[[_Sender], None]) -> None: - """ - Add another handler to this callback. - (Handler should be a callable that takes exactly one parameter: the - sender object.) - """ - # Add to list of event handlers. - self._handlers.append(handler) - - def remove_handler(self, handler: Callable[[_Sender], None]) -> None: - """ - Remove a handler from this callback. - """ - if handler in self._handlers: - self._handlers.remove(handler) - - def __iadd__(self, handler: Callable[[_Sender], None]) -> "Event[_Sender]": - """ - `event += handler` notation for adding a handler. - """ - self.add_handler(handler) - return self - - def __isub__(self, handler: Callable[[_Sender], None]) -> "Event[_Sender]": - """ - `event -= handler` notation for removing a handler. - """ - self.remove_handler(handler) - return self - - -class DummyContext(ContextManager[None]): - """ - (contextlib.nested is not available on Py3) - """ - - def __enter__(self) -> None: - pass - - def __exit__(self, *a: object) -> None: - pass - - -class _CharSizesCache(Dict[str, int]): - """ - Cache for wcwidth sizes. - """ - - LONG_STRING_MIN_LEN = 64 # Minimum string length for considering it long. - MAX_LONG_STRINGS = 16 # Maximum number of long strings to remember. - - def __init__(self) -> None: - super().__init__() - # Keep track of the "long" strings in this cache. - self._long_strings: Deque[str] = deque() - - def __missing__(self, string: str) -> int: - # Note: We use the `max(0, ...` because some non printable control - # characters, like e.g. Ctrl-underscore get a -1 wcwidth value. - # It can be possible that these characters end up in the input - # text. - result: int - if len(string) == 1: - result = max(0, wcwidth(string)) - else: - result = sum(self[c] for c in string) - - # Store in cache. - self[string] = result - - # Rotate long strings. - # (It's hard to tell what we can consider short...) - if len(string) > self.LONG_STRING_MIN_LEN: - long_strings = self._long_strings - long_strings.append(string) - - if len(long_strings) > self.MAX_LONG_STRINGS: - key_to_remove = long_strings.popleft() - if key_to_remove in self: - del self[key_to_remove] - - return result - - -_CHAR_SIZES_CACHE = _CharSizesCache() - - -def get_cwidth(string: str) -> int: - """ - Return width of a string. Wrapper around ``wcwidth``. - """ - return _CHAR_SIZES_CACHE[string] - - -def suspend_to_background_supported() -> bool: - """ - Returns `True` when the Python implementation supports - suspend-to-background. This is typically `False' on Windows systems. - """ - return hasattr(signal, "SIGTSTP") - - -def is_windows() -> bool: - """ - True when we are using Windows. - """ - return sys.platform.startswith("win") # E.g. 'win32', not 'darwin' or 'linux2' - - -def is_windows_vt100_supported() -> bool: - """ - True when we are using Windows, but VT100 escape sequences are supported. - """ - # Import needs to be inline. Windows libraries are not always available. - from prompt_toolkit.output.windows10 import is_win_vt100_enabled - - return is_windows() and is_win_vt100_enabled() - - -def is_conemu_ansi() -> bool: - """ - True when the ConEmu Windows console is used. - """ - return is_windows() and os.environ.get("ConEmuANSI", "OFF") == "ON" - - -def in_main_thread() -> bool: - """ - True when the current thread is the main thread. - """ - return threading.current_thread().__class__.__name__ == "_MainThread" - - -def get_bell_environment_variable() -> bool: - """ - True if env variable is set to true (true, TRUE, TrUe, 1). - """ - value = os.environ.get("PROMPT_TOOLKIT_BELL", "true") - return value.lower() in ("1", "true") - - -def get_term_environment_variable() -> str: - "Return the $TERM environment variable." - return os.environ.get("TERM", "") - - -_T = TypeVar("_T") - - -def take_using_weights( - items: List[_T], weights: List[int] -) -> Generator[_T, None, None]: - """ - Generator that keeps yielding items from the items list, in proportion to - their weight. For instance:: - - # Getting the first 70 items from this generator should have yielded 10 - # times A, 20 times B and 40 times C, all distributed equally.. - take_using_weights(['A', 'B', 'C'], [5, 10, 20]) - - :param items: List of items to take from. - :param weights: Integers representing the weight. (Numbers have to be - integers, not floats.) - """ - assert len(items) == len(weights) - assert len(items) > 0 - - # Remove items with zero-weight. - items2 = [] - weights2 = [] - for item, w in zip(items, weights): - if w > 0: - items2.append(item) - weights2.append(w) - - items = items2 - weights = weights2 - - # Make sure that we have some items left. - if not items: - raise ValueError("Did't got any items with a positive weight.") - - # - already_taken = [0 for i in items] - item_count = len(items) - max_weight = max(weights) - - i = 0 - while True: - # Each iteration of this loop, we fill up until by (total_weight/max_weight). - adding = True - while adding: - adding = False - - for item_i, item, weight in zip(range(item_count), items, weights): - if already_taken[item_i] < i * weight / float(max_weight): - yield item - already_taken[item_i] += 1 - adding = True - - i += 1 - - -def to_str(value: Union[Callable[[], str], str]) -> str: - "Turn callable or string into string." - if callable(value): - return to_str(value()) - else: - return str(value) - - -def to_int(value: Union[Callable[[], int], int]) -> int: - "Turn callable or int into int." - if callable(value): - return to_int(value()) - else: - return int(value) - - -AnyFloat = Union[Callable[[], float], float] - - -def to_float(value: AnyFloat) -> float: - "Turn callable or float into float." - if callable(value): - return to_float(value()) - else: - return float(value) - - -def is_dumb_terminal(term: Optional[str] = None) -> bool: - """ - True if this terminal type is considered "dumb". - - If so, we should fall back to the simplest possible form of line editing, - without cursor positioning and color support. - """ - if term is None: - return is_dumb_terminal(os.environ.get("TERM", "")) - - return term.lower() in ["dumb", "unknown"] +import os +import signal +import sys +import threading +from collections import deque +from typing import ( + Callable, + ContextManager, + Deque, + Dict, + Generator, + Generic, + List, + Optional, + TypeVar, + Union, +) + +from wcwidth import wcwidth + +__all__ = [ + "Event", + "DummyContext", + "get_cwidth", + "suspend_to_background_supported", + "is_conemu_ansi", + "is_windows", + "in_main_thread", + "get_bell_environment_variable", + "get_term_environment_variable", + "take_using_weights", + "to_str", + "to_int", + "AnyFloat", + "to_float", + "is_dumb_terminal", +] + +# Used to ensure sphinx autodoc does not try to import platform-specific +# stuff when documenting win32.py modules. +SPHINX_AUTODOC_RUNNING = "sphinx.ext.autodoc" in sys.modules + +_Sender = TypeVar("_Sender", covariant=True) + + +class Event(Generic[_Sender]): + """ + Simple event to which event handlers can be attached. For instance:: + + class Cls: + def __init__(self): + # Define event. The first parameter is the sender. + self.event = Event(self) + + obj = Cls() + + def handler(sender): + pass + + # Add event handler by using the += operator. + obj.event += handler + + # Fire event. + obj.event() + """ + + def __init__( + self, sender: _Sender, handler: Optional[Callable[[_Sender], None]] = None + ) -> None: + self.sender = sender + self._handlers: List[Callable[[_Sender], None]] = [] + + if handler is not None: + self += handler + + def __call__(self) -> None: + "Fire event." + for handler in self._handlers: + handler(self.sender) + + def fire(self) -> None: + "Alias for just calling the event." + self() + + def add_handler(self, handler: Callable[[_Sender], None]) -> None: + """ + Add another handler to this callback. + (Handler should be a callable that takes exactly one parameter: the + sender object.) + """ + # Add to list of event handlers. + self._handlers.append(handler) + + def remove_handler(self, handler: Callable[[_Sender], None]) -> None: + """ + Remove a handler from this callback. + """ + if handler in self._handlers: + self._handlers.remove(handler) + + def __iadd__(self, handler: Callable[[_Sender], None]) -> "Event[_Sender]": + """ + `event += handler` notation for adding a handler. + """ + self.add_handler(handler) + return self + + def __isub__(self, handler: Callable[[_Sender], None]) -> "Event[_Sender]": + """ + `event -= handler` notation for removing a handler. + """ + self.remove_handler(handler) + return self + + +class DummyContext(ContextManager[None]): + """ + (contextlib.nested is not available on Py3) + """ + + def __enter__(self) -> None: + pass + + def __exit__(self, *a: object) -> None: + pass + + +class _CharSizesCache(Dict[str, int]): + """ + Cache for wcwidth sizes. + """ + + LONG_STRING_MIN_LEN = 64 # Minimum string length for considering it long. + MAX_LONG_STRINGS = 16 # Maximum number of long strings to remember. + + def __init__(self) -> None: + super().__init__() + # Keep track of the "long" strings in this cache. + self._long_strings: Deque[str] = deque() + + def __missing__(self, string: str) -> int: + # Note: We use the `max(0, ...` because some non printable control + # characters, like e.g. Ctrl-underscore get a -1 wcwidth value. + # It can be possible that these characters end up in the input + # text. + result: int + if len(string) == 1: + result = max(0, wcwidth(string)) + else: + result = sum(self[c] for c in string) + + # Store in cache. + self[string] = result + + # Rotate long strings. + # (It's hard to tell what we can consider short...) + if len(string) > self.LONG_STRING_MIN_LEN: + long_strings = self._long_strings + long_strings.append(string) + + if len(long_strings) > self.MAX_LONG_STRINGS: + key_to_remove = long_strings.popleft() + if key_to_remove in self: + del self[key_to_remove] + + return result + + +_CHAR_SIZES_CACHE = _CharSizesCache() + + +def get_cwidth(string: str) -> int: + """ + Return width of a string. Wrapper around ``wcwidth``. + """ + return _CHAR_SIZES_CACHE[string] + + +def suspend_to_background_supported() -> bool: + """ + Returns `True` when the Python implementation supports + suspend-to-background. This is typically `False' on Windows systems. + """ + return hasattr(signal, "SIGTSTP") + + +def is_windows() -> bool: + """ + True when we are using Windows. + """ + return sys.platform.startswith("win") # E.g. 'win32', not 'darwin' or 'linux2' + + +def is_windows_vt100_supported() -> bool: + """ + True when we are using Windows, but VT100 escape sequences are supported. + """ + # Import needs to be inline. Windows libraries are not always available. + from prompt_toolkit.output.windows10 import is_win_vt100_enabled + + return is_windows() and is_win_vt100_enabled() + + +def is_conemu_ansi() -> bool: + """ + True when the ConEmu Windows console is used. + """ + return is_windows() and os.environ.get("ConEmuANSI", "OFF") == "ON" + + +def in_main_thread() -> bool: + """ + True when the current thread is the main thread. + """ + return threading.current_thread().__class__.__name__ == "_MainThread" + + +def get_bell_environment_variable() -> bool: + """ + True if env variable is set to true (true, TRUE, TrUe, 1). + """ + value = os.environ.get("PROMPT_TOOLKIT_BELL", "true") + return value.lower() in ("1", "true") + + +def get_term_environment_variable() -> str: + "Return the $TERM environment variable." + return os.environ.get("TERM", "") + + +_T = TypeVar("_T") + + +def take_using_weights( + items: List[_T], weights: List[int] +) -> Generator[_T, None, None]: + """ + Generator that keeps yielding items from the items list, in proportion to + their weight. For instance:: + + # Getting the first 70 items from this generator should have yielded 10 + # times A, 20 times B and 40 times C, all distributed equally.. + take_using_weights(['A', 'B', 'C'], [5, 10, 20]) + + :param items: List of items to take from. + :param weights: Integers representing the weight. (Numbers have to be + integers, not floats.) + """ + assert len(items) == len(weights) + assert len(items) > 0 + + # Remove items with zero-weight. + items2 = [] + weights2 = [] + for item, w in zip(items, weights): + if w > 0: + items2.append(item) + weights2.append(w) + + items = items2 + weights = weights2 + + # Make sure that we have some items left. + if not items: + raise ValueError("Did't got any items with a positive weight.") + + # + already_taken = [0 for i in items] + item_count = len(items) + max_weight = max(weights) + + i = 0 + while True: + # Each iteration of this loop, we fill up until by (total_weight/max_weight). + adding = True + while adding: + adding = False + + for item_i, item, weight in zip(range(item_count), items, weights): + if already_taken[item_i] < i * weight / float(max_weight): + yield item + already_taken[item_i] += 1 + adding = True + + i += 1 + + +def to_str(value: Union[Callable[[], str], str]) -> str: + "Turn callable or string into string." + if callable(value): + return to_str(value()) + else: + return str(value) + + +def to_int(value: Union[Callable[[], int], int]) -> int: + "Turn callable or int into int." + if callable(value): + return to_int(value()) + else: + return int(value) + + +AnyFloat = Union[Callable[[], float], float] + + +def to_float(value: AnyFloat) -> float: + "Turn callable or float into float." + if callable(value): + return to_float(value()) + else: + return float(value) + + +def is_dumb_terminal(term: Optional[str] = None) -> bool: + """ + True if this terminal type is considered "dumb". + + If so, we should fall back to the simplest possible form of line editing, + without cursor positioning and color support. + """ + if term is None: + return is_dumb_terminal(os.environ.get("TERM", "")) + + return term.lower() in ["dumb", "unknown"] diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/validation.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/validation.py index 386d0e87af..db2f3fd1c6 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/validation.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/validation.py @@ -1,194 +1,194 @@ -""" -Input validation for a `Buffer`. -(Validators will be called before accepting input.) -""" -from abc import ABCMeta, abstractmethod -from typing import Callable, Optional - -from prompt_toolkit.eventloop import run_in_executor_with_context - -from .document import Document -from .filters import FilterOrBool, to_filter - -__all__ = [ - "ConditionalValidator", - "ValidationError", - "Validator", - "ThreadedValidator", - "DummyValidator", - "DynamicValidator", -] - - -class ValidationError(Exception): - """ - Error raised by :meth:`.Validator.validate`. - - :param cursor_position: The cursor position where the error occurred. - :param message: Text. - """ - - def __init__(self, cursor_position: int = 0, message: str = "") -> None: - super().__init__(message) - self.cursor_position = cursor_position - self.message = message - - def __repr__(self) -> str: - return "%s(cursor_position=%r, message=%r)" % ( - self.__class__.__name__, - self.cursor_position, - self.message, - ) - - -class Validator(metaclass=ABCMeta): - """ - Abstract base class for an input validator. - - A validator is typically created in one of the following two ways: - - - Either by overriding this class and implementing the `validate` method. - - Or by passing a callable to `Validator.from_callable`. - - If the validation takes some time and needs to happen in a background - thread, this can be wrapped in a :class:`.ThreadedValidator`. - """ - - @abstractmethod - def validate(self, document: Document) -> None: - """ - Validate the input. - If invalid, this should raise a :class:`.ValidationError`. - - :param document: :class:`~prompt_toolkit.document.Document` instance. - """ - pass - - async def validate_async(self, document: Document) -> None: - """ - Return a `Future` which is set when the validation is ready. - This function can be overloaded in order to provide an asynchronous - implementation. - """ - try: - self.validate(document) - except ValidationError: - raise - - @classmethod - def from_callable( - cls, - validate_func: Callable[[str], bool], - error_message: str = "Invalid input", - move_cursor_to_end: bool = False, - ) -> "Validator": - """ - Create a validator from a simple validate callable. E.g.: - - .. code:: python - - def is_valid(text): - return text in ['hello', 'world'] - Validator.from_callable(is_valid, error_message='Invalid input') - - :param validate_func: Callable that takes the input string, and returns - `True` if the input is valid input. - :param error_message: Message to be displayed if the input is invalid. - :param move_cursor_to_end: Move the cursor to the end of the input, if - the input is invalid. - """ - return _ValidatorFromCallable(validate_func, error_message, move_cursor_to_end) - - -class _ValidatorFromCallable(Validator): - """ - Validate input from a simple callable. - """ - - def __init__( - self, func: Callable[[str], bool], error_message: str, move_cursor_to_end: bool - ) -> None: - - self.func = func - self.error_message = error_message - self.move_cursor_to_end = move_cursor_to_end - - def __repr__(self) -> str: - return "Validator.from_callable(%r)" % (self.func,) - - def validate(self, document: Document) -> None: - if not self.func(document.text): - if self.move_cursor_to_end: - index = len(document.text) - else: - index = 0 - - raise ValidationError(cursor_position=index, message=self.error_message) - - -class ThreadedValidator(Validator): - """ - Wrapper that runs input validation in a thread. - (Use this to prevent the user interface from becoming unresponsive if the - input validation takes too much time.) - """ - - def __init__(self, validator: Validator) -> None: - self.validator = validator - - def validate(self, document: Document) -> None: - self.validator.validate(document) - - async def validate_async(self, document: Document) -> None: - """ - Run the `validate` function in a thread. - """ - - def run_validation_thread() -> None: - return self.validate(document) - - await run_in_executor_with_context(run_validation_thread) - - -class DummyValidator(Validator): - """ - Validator class that accepts any input. - """ - - def validate(self, document: Document) -> None: - pass # Don't raise any exception. - - -class ConditionalValidator(Validator): - """ - Validator that can be switched on/off according to - a filter. (This wraps around another validator.) - """ - - def __init__(self, validator: Validator, filter: FilterOrBool) -> None: - self.validator = validator - self.filter = to_filter(filter) - - def validate(self, document: Document) -> None: - # Call the validator only if the filter is active. - if self.filter(): - self.validator.validate(document) - - -class DynamicValidator(Validator): - """ - Validator class that can dynamically returns any Validator. - - :param get_validator: Callable that returns a :class:`.Validator` instance. - """ - - def __init__(self, get_validator: Callable[[], Optional[Validator]]) -> None: - self.get_validator = get_validator - - def validate(self, document: Document) -> None: - validator = self.get_validator() or DummyValidator() - validator.validate(document) - - async def validate_async(self, document: Document) -> None: - validator = self.get_validator() or DummyValidator() - await validator.validate_async(document) +""" +Input validation for a `Buffer`. +(Validators will be called before accepting input.) +""" +from abc import ABCMeta, abstractmethod +from typing import Callable, Optional + +from prompt_toolkit.eventloop import run_in_executor_with_context + +from .document import Document +from .filters import FilterOrBool, to_filter + +__all__ = [ + "ConditionalValidator", + "ValidationError", + "Validator", + "ThreadedValidator", + "DummyValidator", + "DynamicValidator", +] + + +class ValidationError(Exception): + """ + Error raised by :meth:`.Validator.validate`. + + :param cursor_position: The cursor position where the error occurred. + :param message: Text. + """ + + def __init__(self, cursor_position: int = 0, message: str = "") -> None: + super().__init__(message) + self.cursor_position = cursor_position + self.message = message + + def __repr__(self) -> str: + return "%s(cursor_position=%r, message=%r)" % ( + self.__class__.__name__, + self.cursor_position, + self.message, + ) + + +class Validator(metaclass=ABCMeta): + """ + Abstract base class for an input validator. + + A validator is typically created in one of the following two ways: + + - Either by overriding this class and implementing the `validate` method. + - Or by passing a callable to `Validator.from_callable`. + + If the validation takes some time and needs to happen in a background + thread, this can be wrapped in a :class:`.ThreadedValidator`. + """ + + @abstractmethod + def validate(self, document: Document) -> None: + """ + Validate the input. + If invalid, this should raise a :class:`.ValidationError`. + + :param document: :class:`~prompt_toolkit.document.Document` instance. + """ + pass + + async def validate_async(self, document: Document) -> None: + """ + Return a `Future` which is set when the validation is ready. + This function can be overloaded in order to provide an asynchronous + implementation. + """ + try: + self.validate(document) + except ValidationError: + raise + + @classmethod + def from_callable( + cls, + validate_func: Callable[[str], bool], + error_message: str = "Invalid input", + move_cursor_to_end: bool = False, + ) -> "Validator": + """ + Create a validator from a simple validate callable. E.g.: + + .. code:: python + + def is_valid(text): + return text in ['hello', 'world'] + Validator.from_callable(is_valid, error_message='Invalid input') + + :param validate_func: Callable that takes the input string, and returns + `True` if the input is valid input. + :param error_message: Message to be displayed if the input is invalid. + :param move_cursor_to_end: Move the cursor to the end of the input, if + the input is invalid. + """ + return _ValidatorFromCallable(validate_func, error_message, move_cursor_to_end) + + +class _ValidatorFromCallable(Validator): + """ + Validate input from a simple callable. + """ + + def __init__( + self, func: Callable[[str], bool], error_message: str, move_cursor_to_end: bool + ) -> None: + + self.func = func + self.error_message = error_message + self.move_cursor_to_end = move_cursor_to_end + + def __repr__(self) -> str: + return "Validator.from_callable(%r)" % (self.func,) + + def validate(self, document: Document) -> None: + if not self.func(document.text): + if self.move_cursor_to_end: + index = len(document.text) + else: + index = 0 + + raise ValidationError(cursor_position=index, message=self.error_message) + + +class ThreadedValidator(Validator): + """ + Wrapper that runs input validation in a thread. + (Use this to prevent the user interface from becoming unresponsive if the + input validation takes too much time.) + """ + + def __init__(self, validator: Validator) -> None: + self.validator = validator + + def validate(self, document: Document) -> None: + self.validator.validate(document) + + async def validate_async(self, document: Document) -> None: + """ + Run the `validate` function in a thread. + """ + + def run_validation_thread() -> None: + return self.validate(document) + + await run_in_executor_with_context(run_validation_thread) + + +class DummyValidator(Validator): + """ + Validator class that accepts any input. + """ + + def validate(self, document: Document) -> None: + pass # Don't raise any exception. + + +class ConditionalValidator(Validator): + """ + Validator that can be switched on/off according to + a filter. (This wraps around another validator.) + """ + + def __init__(self, validator: Validator, filter: FilterOrBool) -> None: + self.validator = validator + self.filter = to_filter(filter) + + def validate(self, document: Document) -> None: + # Call the validator only if the filter is active. + if self.filter(): + self.validator.validate(document) + + +class DynamicValidator(Validator): + """ + Validator class that can dynamically returns any Validator. + + :param get_validator: Callable that returns a :class:`.Validator` instance. + """ + + def __init__(self, get_validator: Callable[[], Optional[Validator]]) -> None: + self.get_validator = get_validator + + def validate(self, document: Document) -> None: + validator = self.get_validator() or DummyValidator() + validator.validate(document) + + async def validate_async(self, document: Document) -> None: + validator = self.get_validator() or DummyValidator() + await validator.validate_async(document) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/__init__.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/__init__.py index a7559ebd0f..552d355948 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/__init__.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/__init__.py @@ -1,60 +1,60 @@ -""" -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 .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", -] +""" +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 .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/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/base.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/base.py index c059872793..728190b54c 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/base.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/base.py @@ -1,949 +1,949 @@ -""" -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 functools import partial -from typing import Callable, Generic, List, Optional, Sequence, Tuple, TypeVar, Union - -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 -from prompt_toolkit.layout.dimension import Dimension as D -from prompt_toolkit.layout.dimension import to_dimension -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: Optional[Lexer] = None, - auto_suggest: Optional[AutoSuggest] = None, - completer: Optional[Completer] = None, - complete_while_typing: FilterOrBool = True, - validator: Optional[Validator] = None, - accept_handler: Optional[BufferAcceptHandler] = None, - history: Optional[History] = 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: Optional[GetLinePrefixCallable] = None, - scrollbar: bool = False, - style: str = "", - search_field: Optional[SearchToolbar] = None, - preview_search: FilterOrBool = True, - prompt: AnyFormattedText = "", - input_processors: Optional[List[Processor]] = None, - ) -> 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, - ) - - 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) -> Optional[BufferAcceptHandler]: - """ - 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, +""" +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 functools import partial +from typing import Callable, Generic, List, Optional, Sequence, Tuple, TypeVar, Union + +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 +from prompt_toolkit.layout.dimension import Dimension as D +from prompt_toolkit.layout.dimension import to_dimension +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: Optional[Lexer] = None, + auto_suggest: Optional[AutoSuggest] = None, + completer: Optional[Completer] = None, + complete_while_typing: FilterOrBool = True, + validator: Optional[Validator] = None, + accept_handler: Optional[BufferAcceptHandler] = None, + history: Optional[History] = 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: Optional[GetLinePrefixCallable] = None, + scrollbar: bool = False, + style: str = "", + search_field: Optional[SearchToolbar] = None, + preview_search: FilterOrBool = True, + prompt: AnyFormattedText = "", + input_processors: Optional[List[Processor]] = None, + ) -> 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, + ) + + 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) -> Optional[BufferAcceptHandler]: + """ + 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: Union[WindowAlign, Callable[[], WindowAlign]] = WindowAlign.LEFT, - ) -> 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, + ) -> 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, - ) - - 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: Optional[Callable[[], 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 = ("{{:^{}}}".format(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: Optional[KeyBindings] = 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: Union[None, str, Callable[[], str]] = None, - modal: bool = False, - key_bindings: Optional[KeyBindings] = 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]]) -> None: - assert len(values) > 0 - - self.values = values - # current_values will be used in multiple_selection, - # current_value will be used otherwise. - self.current_values: List[_T] = [] - self.current_value: _T = values[0][0] - 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 - - -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)] - CheckboxList.__init__(self, 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(object): - """ - 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 = "{0}%".format(value) - - def __pt_container__(self) -> Container: - return self.container + ) + + 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: Optional[Callable[[], 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 = ("{{:^{}}}".format(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: Optional[KeyBindings] = 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: Union[None, str, Callable[[], str]] = None, + modal: bool = False, + key_bindings: Optional[KeyBindings] = 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]]) -> None: + assert len(values) > 0 + + self.values = values + # current_values will be used in multiple_selection, + # current_value will be used otherwise. + self.current_values: List[_T] = [] + self.current_value: _T = values[0][0] + 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 + + +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)] + CheckboxList.__init__(self, 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(object): + """ + 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 = "{0}%".format(value) + + def __pt_container__(self) -> Container: + return self.container diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/dialogs.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/dialogs.py index d814d275ea..920582b4e6 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/dialogs.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/dialogs.py @@ -1,106 +1,106 @@ -""" -Collection of reusable components for building full screen applications. -""" -from typing import Optional, Sequence, Union - -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: Optional[Sequence[Button]] = 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: Union[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 +""" +Collection of reusable components for building full screen applications. +""" +from typing import Optional, Sequence, Union + +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: Optional[Sequence[Button]] = 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: Union[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/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/menus.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/menus.py index f4d91067c9..7203aae118 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/menus.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/menus.py @@ -1,374 +1,374 @@ -from typing import Callable, Iterable, List, Optional, Sequence, Union - -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: Optional[List[Float]] = None, - key_bindings: Optional[KeyBindingsBase] = 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", - "{}".format(Border.HORIZONTAL * (menu.width + 3)), - mouse_handler, - ) - else: - yield ( - style, - " {}".format(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) -> Optional[List[Float]]: - return self.container.floats - - def __pt_container__(self) -> Container: - return self.container - - -class MenuItem: - def __init__( - self, - text: str = "", - handler: Optional[Callable[[], None]] = None, - children: Optional[List["MenuItem"]] = None, - shortcut: Optional[Sequence[Union[Keys, str]]] = 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 +from typing import Callable, Iterable, List, Optional, Sequence, Union + +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: Optional[List[Float]] = None, + key_bindings: Optional[KeyBindingsBase] = 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", + "{}".format(Border.HORIZONTAL * (menu.width + 3)), + mouse_handler, + ) + else: + yield ( + style, + " {}".format(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) -> Optional[List[Float]]: + return self.container.floats + + def __pt_container__(self) -> Container: + return self.container + + +class MenuItem: + def __init__( + self, + text: str = "", + handler: Optional[Callable[[], None]] = None, + children: Optional[List["MenuItem"]] = None, + shortcut: Optional[Sequence[Union[Keys, str]]] = 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/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/toolbars.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/toolbars.py index eaf29b5b18..c3559c2b83 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/toolbars.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/widgets/toolbars.py @@ -1,374 +1,374 @@ -from typing import Any, Optional - -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 - 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: Optional[Buffer] = 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 = "%s (line=%s column=%s)" % ( - 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 +from typing import Any, Optional + +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 + 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: Optional[Buffer] = 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 = "%s (line=%s column=%s)" % ( + 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 diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/win32_types.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/win32_types.py index 05c4f88510..e721e5ef7c 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/win32_types.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/win32_types.py @@ -1,227 +1,227 @@ -from ctypes import Structure, Union, c_char, c_long, c_short, c_ulong -from ctypes.wintypes import BOOL, DWORD, LPVOID, WCHAR, WORD -from typing import TYPE_CHECKING - -# Input/Output standard device numbers. Note that these are not handle objects. -# It's the `windll.kernel32.GetStdHandle` system call that turns them into a -# real handle object. -STD_INPUT_HANDLE = c_ulong(-10) -STD_OUTPUT_HANDLE = c_ulong(-11) -STD_ERROR_HANDLE = c_ulong(-12) - - -class COORD(Structure): - """ - Struct in wincon.h - http://msdn.microsoft.com/en-us/library/windows/desktop/ms682119(v=vs.85).aspx - """ - - if TYPE_CHECKING: - X: int - Y: int - - _fields_ = [ - ("X", c_short), # Short - ("Y", c_short), # Short - ] - - def __repr__(self) -> str: - return "%s(X=%r, Y=%r, type_x=%r, type_y=%r)" % ( - self.__class__.__name__, - self.X, - self.Y, - type(self.X), - type(self.Y), - ) - - -class UNICODE_OR_ASCII(Union): - if TYPE_CHECKING: - AsciiChar: bytes - UnicodeChar: str - - _fields_ = [ - ("AsciiChar", c_char), - ("UnicodeChar", WCHAR), - ] - - -class KEY_EVENT_RECORD(Structure): - """ - http://msdn.microsoft.com/en-us/library/windows/desktop/ms684166(v=vs.85).aspx - """ - - if TYPE_CHECKING: - KeyDown: int - RepeatCount: int - VirtualKeyCode: int - VirtualScanCode: int - uChar: UNICODE_OR_ASCII - ControlKeyState: int - - _fields_ = [ - ("KeyDown", c_long), # bool - ("RepeatCount", c_short), # word - ("VirtualKeyCode", c_short), # word - ("VirtualScanCode", c_short), # word - ("uChar", UNICODE_OR_ASCII), # Unicode or ASCII. - ("ControlKeyState", c_long), # double word - ] - - -class MOUSE_EVENT_RECORD(Structure): - """ - http://msdn.microsoft.com/en-us/library/windows/desktop/ms684239(v=vs.85).aspx - """ - - if TYPE_CHECKING: - MousePosition: COORD - ButtonState: int - ControlKeyState: int - EventFlags: int - - _fields_ = [ - ("MousePosition", COORD), - ("ButtonState", c_long), # dword - ("ControlKeyState", c_long), # dword - ("EventFlags", c_long), # dword - ] - - -class WINDOW_BUFFER_SIZE_RECORD(Structure): - """ - http://msdn.microsoft.com/en-us/library/windows/desktop/ms687093(v=vs.85).aspx - """ - - if TYPE_CHECKING: - Size: COORD - - _fields_ = [("Size", COORD)] - - -class MENU_EVENT_RECORD(Structure): - """ - http://msdn.microsoft.com/en-us/library/windows/desktop/ms684213(v=vs.85).aspx - """ - - if TYPE_CHECKING: - CommandId: int - - _fields_ = [("CommandId", c_long)] # uint - - -class FOCUS_EVENT_RECORD(Structure): - """ - http://msdn.microsoft.com/en-us/library/windows/desktop/ms683149(v=vs.85).aspx - """ - - if TYPE_CHECKING: - SetFocus: int - - _fields_ = [("SetFocus", c_long)] # bool - - -class EVENT_RECORD(Union): - if TYPE_CHECKING: - KeyEvent: KEY_EVENT_RECORD - MouseEvent: MOUSE_EVENT_RECORD - WindowBufferSizeEvent: WINDOW_BUFFER_SIZE_RECORD - MenuEvent: MENU_EVENT_RECORD - FocusEvent: FOCUS_EVENT_RECORD - - _fields_ = [ - ("KeyEvent", KEY_EVENT_RECORD), - ("MouseEvent", MOUSE_EVENT_RECORD), - ("WindowBufferSizeEvent", WINDOW_BUFFER_SIZE_RECORD), - ("MenuEvent", MENU_EVENT_RECORD), - ("FocusEvent", FOCUS_EVENT_RECORD), - ] - - -class INPUT_RECORD(Structure): - """ - http://msdn.microsoft.com/en-us/library/windows/desktop/ms683499(v=vs.85).aspx - """ - - if TYPE_CHECKING: - EventType: int - Event: EVENT_RECORD - - _fields_ = [("EventType", c_short), ("Event", EVENT_RECORD)] # word # Union. - - -EventTypes = { - 1: "KeyEvent", - 2: "MouseEvent", - 4: "WindowBufferSizeEvent", - 8: "MenuEvent", - 16: "FocusEvent", -} - - -class SMALL_RECT(Structure): - """struct in wincon.h.""" - - if TYPE_CHECKING: - Left: int - Top: int - Right: int - Bottom: int - - _fields_ = [ - ("Left", c_short), - ("Top", c_short), - ("Right", c_short), - ("Bottom", c_short), - ] - - -class CONSOLE_SCREEN_BUFFER_INFO(Structure): - """struct in wincon.h.""" - - if TYPE_CHECKING: - dwSize: COORD - dwCursorPosition: COORD - wAttributes: int - srWindow: SMALL_RECT - dwMaximumWindowSize: COORD - - _fields_ = [ - ("dwSize", COORD), - ("dwCursorPosition", COORD), - ("wAttributes", WORD), - ("srWindow", SMALL_RECT), - ("dwMaximumWindowSize", COORD), - ] - - def __repr__(self) -> str: - return "CONSOLE_SCREEN_BUFFER_INFO(%r,%r,%r,%r,%r,%r,%r,%r,%r,%r,%r)" % ( - self.dwSize.Y, - self.dwSize.X, - self.dwCursorPosition.Y, - self.dwCursorPosition.X, - self.wAttributes, - self.srWindow.Top, - self.srWindow.Left, - self.srWindow.Bottom, - self.srWindow.Right, - self.dwMaximumWindowSize.Y, - self.dwMaximumWindowSize.X, - ) - - -class SECURITY_ATTRIBUTES(Structure): - """ - http://msdn.microsoft.com/en-us/library/windows/desktop/aa379560(v=vs.85).aspx - """ - - if TYPE_CHECKING: - nLength: int - lpSecurityDescriptor: int - bInheritHandle: int # BOOL comes back as 'int'. - - _fields_ = [ - ("nLength", DWORD), - ("lpSecurityDescriptor", LPVOID), - ("bInheritHandle", BOOL), - ] +from ctypes import Structure, Union, c_char, c_long, c_short, c_ulong +from ctypes.wintypes import BOOL, DWORD, LPVOID, WCHAR, WORD +from typing import TYPE_CHECKING + +# Input/Output standard device numbers. Note that these are not handle objects. +# It's the `windll.kernel32.GetStdHandle` system call that turns them into a +# real handle object. +STD_INPUT_HANDLE = c_ulong(-10) +STD_OUTPUT_HANDLE = c_ulong(-11) +STD_ERROR_HANDLE = c_ulong(-12) + + +class COORD(Structure): + """ + Struct in wincon.h + http://msdn.microsoft.com/en-us/library/windows/desktop/ms682119(v=vs.85).aspx + """ + + if TYPE_CHECKING: + X: int + Y: int + + _fields_ = [ + ("X", c_short), # Short + ("Y", c_short), # Short + ] + + def __repr__(self) -> str: + return "%s(X=%r, Y=%r, type_x=%r, type_y=%r)" % ( + self.__class__.__name__, + self.X, + self.Y, + type(self.X), + type(self.Y), + ) + + +class UNICODE_OR_ASCII(Union): + if TYPE_CHECKING: + AsciiChar: bytes + UnicodeChar: str + + _fields_ = [ + ("AsciiChar", c_char), + ("UnicodeChar", WCHAR), + ] + + +class KEY_EVENT_RECORD(Structure): + """ + http://msdn.microsoft.com/en-us/library/windows/desktop/ms684166(v=vs.85).aspx + """ + + if TYPE_CHECKING: + KeyDown: int + RepeatCount: int + VirtualKeyCode: int + VirtualScanCode: int + uChar: UNICODE_OR_ASCII + ControlKeyState: int + + _fields_ = [ + ("KeyDown", c_long), # bool + ("RepeatCount", c_short), # word + ("VirtualKeyCode", c_short), # word + ("VirtualScanCode", c_short), # word + ("uChar", UNICODE_OR_ASCII), # Unicode or ASCII. + ("ControlKeyState", c_long), # double word + ] + + +class MOUSE_EVENT_RECORD(Structure): + """ + http://msdn.microsoft.com/en-us/library/windows/desktop/ms684239(v=vs.85).aspx + """ + + if TYPE_CHECKING: + MousePosition: COORD + ButtonState: int + ControlKeyState: int + EventFlags: int + + _fields_ = [ + ("MousePosition", COORD), + ("ButtonState", c_long), # dword + ("ControlKeyState", c_long), # dword + ("EventFlags", c_long), # dword + ] + + +class WINDOW_BUFFER_SIZE_RECORD(Structure): + """ + http://msdn.microsoft.com/en-us/library/windows/desktop/ms687093(v=vs.85).aspx + """ + + if TYPE_CHECKING: + Size: COORD + + _fields_ = [("Size", COORD)] + + +class MENU_EVENT_RECORD(Structure): + """ + http://msdn.microsoft.com/en-us/library/windows/desktop/ms684213(v=vs.85).aspx + """ + + if TYPE_CHECKING: + CommandId: int + + _fields_ = [("CommandId", c_long)] # uint + + +class FOCUS_EVENT_RECORD(Structure): + """ + http://msdn.microsoft.com/en-us/library/windows/desktop/ms683149(v=vs.85).aspx + """ + + if TYPE_CHECKING: + SetFocus: int + + _fields_ = [("SetFocus", c_long)] # bool + + +class EVENT_RECORD(Union): + if TYPE_CHECKING: + KeyEvent: KEY_EVENT_RECORD + MouseEvent: MOUSE_EVENT_RECORD + WindowBufferSizeEvent: WINDOW_BUFFER_SIZE_RECORD + MenuEvent: MENU_EVENT_RECORD + FocusEvent: FOCUS_EVENT_RECORD + + _fields_ = [ + ("KeyEvent", KEY_EVENT_RECORD), + ("MouseEvent", MOUSE_EVENT_RECORD), + ("WindowBufferSizeEvent", WINDOW_BUFFER_SIZE_RECORD), + ("MenuEvent", MENU_EVENT_RECORD), + ("FocusEvent", FOCUS_EVENT_RECORD), + ] + + +class INPUT_RECORD(Structure): + """ + http://msdn.microsoft.com/en-us/library/windows/desktop/ms683499(v=vs.85).aspx + """ + + if TYPE_CHECKING: + EventType: int + Event: EVENT_RECORD + + _fields_ = [("EventType", c_short), ("Event", EVENT_RECORD)] # word # Union. + + +EventTypes = { + 1: "KeyEvent", + 2: "MouseEvent", + 4: "WindowBufferSizeEvent", + 8: "MenuEvent", + 16: "FocusEvent", +} + + +class SMALL_RECT(Structure): + """struct in wincon.h.""" + + if TYPE_CHECKING: + Left: int + Top: int + Right: int + Bottom: int + + _fields_ = [ + ("Left", c_short), + ("Top", c_short), + ("Right", c_short), + ("Bottom", c_short), + ] + + +class CONSOLE_SCREEN_BUFFER_INFO(Structure): + """struct in wincon.h.""" + + if TYPE_CHECKING: + dwSize: COORD + dwCursorPosition: COORD + wAttributes: int + srWindow: SMALL_RECT + dwMaximumWindowSize: COORD + + _fields_ = [ + ("dwSize", COORD), + ("dwCursorPosition", COORD), + ("wAttributes", WORD), + ("srWindow", SMALL_RECT), + ("dwMaximumWindowSize", COORD), + ] + + def __repr__(self) -> str: + return "CONSOLE_SCREEN_BUFFER_INFO(%r,%r,%r,%r,%r,%r,%r,%r,%r,%r,%r)" % ( + self.dwSize.Y, + self.dwSize.X, + self.dwCursorPosition.Y, + self.dwCursorPosition.X, + self.wAttributes, + self.srWindow.Top, + self.srWindow.Left, + self.srWindow.Bottom, + self.srWindow.Right, + self.dwMaximumWindowSize.Y, + self.dwMaximumWindowSize.X, + ) + + +class SECURITY_ATTRIBUTES(Structure): + """ + http://msdn.microsoft.com/en-us/library/windows/desktop/aa379560(v=vs.85).aspx + """ + + if TYPE_CHECKING: + nLength: int + lpSecurityDescriptor: int + bInheritHandle: int # BOOL comes back as 'int'. + + _fields_ = [ + ("nLength", DWORD), + ("lpSecurityDescriptor", LPVOID), + ("bInheritHandle", BOOL), + ] |