summaryrefslogtreecommitdiffstats
path: root/src/prompt_toolkit/filters
diff options
context:
space:
mode:
Diffstat (limited to 'src/prompt_toolkit/filters')
-rw-r--r--src/prompt_toolkit/filters/__init__.py70
-rw-r--r--src/prompt_toolkit/filters/app.py418
-rw-r--r--src/prompt_toolkit/filters/base.py255
-rw-r--r--src/prompt_toolkit/filters/cli.py64
-rw-r--r--src/prompt_toolkit/filters/utils.py41
5 files changed, 848 insertions, 0 deletions
diff --git a/src/prompt_toolkit/filters/__init__.py b/src/prompt_toolkit/filters/__init__.py
new file mode 100644
index 0000000..277f428
--- /dev/null
+++ b/src/prompt_toolkit/filters/__init__.py
@@ -0,0 +1,70 @@
+"""
+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 __future__ import annotations
+
+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/src/prompt_toolkit/filters/app.py b/src/prompt_toolkit/filters/app.py
new file mode 100644
index 0000000..aacb228
--- /dev/null
+++ b/src/prompt_toolkit/filters/app.py
@@ -0,0 +1,418 @@
+"""
+Filters that accept a `Application` as argument.
+"""
+from __future__ import annotations
+
+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_suggestion",
+ "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",
+]
+
+
+# NOTE: `has_focus` below should *not* be `memoized`. It can reference any user
+# control. For instance, if we would continuously create new
+# `PromptSession` instances, then previous instances won't be released,
+# because this memoize (which caches results in the global scope) will
+# still refer to each instance.
+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_suggestion() -> bool:
+ """
+ Enable when the current buffer has a suggestion.
+ """
+ buffer = get_app().current_buffer
+ return buffer.suggestion is not None and buffer.suggestion.text != ""
+
+
+@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/src/prompt_toolkit/filters/base.py b/src/prompt_toolkit/filters/base.py
new file mode 100644
index 0000000..afce6dc
--- /dev/null
+++ b/src/prompt_toolkit/filters/base.py
@@ -0,0 +1,255 @@
+from __future__ import annotations
+
+from abc import ABCMeta, abstractmethod
+from typing import Callable, Iterable, 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.
+ """
+
+ def __init__(self) -> None:
+ self._and_cache: dict[Filter, Filter] = {}
+ self._or_cache: dict[Filter, Filter] = {}
+ self._invert_result: Filter | None = None
+
+ @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.
+ """
+ assert isinstance(other, Filter), "Expecting filter, got %r" % other
+
+ if isinstance(other, Always):
+ return self
+ if isinstance(other, Never):
+ return other
+
+ if other in self._and_cache:
+ return self._and_cache[other]
+
+ result = _AndList.create([self, other])
+ self._and_cache[other] = result
+ return result
+
+ def __or__(self, other: Filter) -> Filter:
+ """
+ Chaining of filters using the | operator.
+ """
+ assert isinstance(other, Filter), "Expecting filter, got %r" % other
+
+ if isinstance(other, Always):
+ return other
+ if isinstance(other, Never):
+ return self
+
+ if other in self._or_cache:
+ return self._or_cache[other]
+
+ result = _OrList.create([self, other])
+ self._or_cache[other] = result
+ return result
+
+ def __invert__(self) -> Filter:
+ """
+ Inverting of filters using the ~ operator.
+ """
+ if self._invert_result is None:
+ self._invert_result = _Invert(self)
+
+ return self._invert_result
+
+ 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."
+ )
+
+
+def _remove_duplicates(filters: list[Filter]) -> list[Filter]:
+ result = []
+ for f in filters:
+ if f not in result:
+ result.append(f)
+ return result
+
+
+class _AndList(Filter):
+ """
+ Result of &-operation between several filters.
+ """
+
+ def __init__(self, filters: list[Filter]) -> None:
+ super().__init__()
+ self.filters = filters
+
+ @classmethod
+ def create(cls, filters: Iterable[Filter]) -> Filter:
+ """
+ Create a new filter by applying an `&` operator between them.
+
+ If there's only one unique filter in the given iterable, it will return
+ that one filter instead of an `_AndList`.
+ """
+ filters_2: list[Filter] = []
+
+ for f in filters:
+ if isinstance(f, _AndList): # Turn nested _AndLists into one.
+ filters_2.extend(f.filters)
+ else:
+ filters_2.append(f)
+
+ # Remove duplicates. This could speed up execution, and doesn't make a
+ # difference for the evaluation.
+ filters = _remove_duplicates(filters_2)
+
+ # If only one filter is left, return that without wrapping into an
+ # `_AndList`.
+ if len(filters) == 1:
+ return filters[0]
+
+ return cls(filters)
+
+ 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: list[Filter]) -> None:
+ super().__init__()
+ self.filters = filters
+
+ @classmethod
+ def create(cls, filters: Iterable[Filter]) -> Filter:
+ """
+ Create a new filter by applying an `|` operator between them.
+
+ If there's only one unique filter in the given iterable, it will return
+ that one filter instead of an `_OrList`.
+ """
+ filters_2: list[Filter] = []
+
+ for f in filters:
+ if isinstance(f, _OrList): # Turn nested _AndLists into one.
+ filters_2.extend(f.filters)
+ else:
+ filters_2.append(f)
+
+ # Remove duplicates. This could speed up execution, and doesn't make a
+ # difference for the evaluation.
+ filters = _remove_duplicates(filters_2)
+
+ # If only one filter is left, return that without wrapping into an
+ # `_AndList`.
+ if len(filters) == 1:
+ return filters[0]
+
+ return cls(filters)
+
+ 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:
+ super().__init__()
+ 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 __or__(self, other: Filter) -> Filter:
+ return self
+
+ def __invert__(self) -> Never:
+ return Never()
+
+
+class Never(Filter):
+ """
+ Never enable feature.
+ """
+
+ def __call__(self) -> bool:
+ return False
+
+ def __and__(self, other: Filter) -> Filter:
+ return self
+
+ 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:
+ super().__init__()
+ 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/src/prompt_toolkit/filters/cli.py b/src/prompt_toolkit/filters/cli.py
new file mode 100644
index 0000000..c95080a
--- /dev/null
+++ b/src/prompt_toolkit/filters/cli.py
@@ -0,0 +1,64 @@
+"""
+For backwards-compatibility. keep this file.
+(Many people are going to have key bindings that rely on this file.)
+"""
+from __future__ import annotations
+
+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/src/prompt_toolkit/filters/utils.py b/src/prompt_toolkit/filters/utils.py
new file mode 100644
index 0000000..bac85ba
--- /dev/null
+++ b/src/prompt_toolkit/filters/utils.py
@@ -0,0 +1,41 @@
+from __future__ import annotations
+
+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)()