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__.py68
-rw-r--r--src/prompt_toolkit/filters/app.py412
-rw-r--r--src/prompt_toolkit/filters/base.py217
-rw-r--r--src/prompt_toolkit/filters/cli.py62
-rw-r--r--src/prompt_toolkit/filters/utils.py41
5 files changed, 800 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..d97195f
--- /dev/null
+++ b/src/prompt_toolkit/filters/__init__.py
@@ -0,0 +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)
diff --git a/src/prompt_toolkit/filters/app.py b/src/prompt_toolkit/filters/app.py
new file mode 100644
index 0000000..dcc3fc0
--- /dev/null
+++ b/src/prompt_toolkit/filters/app.py
@@ -0,0 +1,412 @@
+"""
+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_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",
+]
+
+
+@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_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..fd57cca
--- /dev/null
+++ b/src/prompt_toolkit/filters/base.py
@@ -0,0 +1,217 @@
+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]
diff --git a/src/prompt_toolkit/filters/cli.py b/src/prompt_toolkit/filters/cli.py
new file mode 100644
index 0000000..7135196
--- /dev/null
+++ b/src/prompt_toolkit/filters/cli.py
@@ -0,0 +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
diff --git a/src/prompt_toolkit/filters/utils.py b/src/prompt_toolkit/filters/utils.py
new file mode 100644
index 0000000..aaf44aa
--- /dev/null
+++ b/src/prompt_toolkit/filters/utils.py
@@ -0,0 +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)()