Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion prompt_toolkit/application/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .application import Application
from .application import Application, handle_exit_on_key
from .current import (
AppSession,
create_app_session,
Expand All @@ -13,6 +13,7 @@
__all__ = [
# Application.
"Application",
"handle_exit_on_key",
# Current.
"AppSession",
"get_app_session",
Expand Down
26 changes: 26 additions & 0 deletions prompt_toolkit/application/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ class Application(Generic[_AppResult]):
:param key_bindings:
:class:`~prompt_toolkit.key_binding.KeyBindingsBase` instance for
the key bindings.
:param exit_on_key: (str|tuple) auto-bind key to exit the application
:param clipboard: :class:`~prompt_toolkit.clipboard.Clipboard` to use.
:param on_abort: What to do when Control-C is pressed.
:param on_exit: What to do when Control-D is pressed.
Expand Down Expand Up @@ -195,6 +196,7 @@ def __init__(
include_default_pygments_style: FilterOrBool = True,
style_transformation: Optional[StyleTransformation] = None,
key_bindings: Optional[KeyBindingsBase] = None,
exit_on_key: Optional[Union[str, tuple]] = None,
clipboard: Optional[Clipboard] = None,
full_screen: bool = False,
color_depth: Union[
Expand Down Expand Up @@ -242,6 +244,8 @@ def __init__(

# Key bindings.
self.key_bindings = key_bindings
self.exit_on_key = (exit_on_key,) if type(exit_on_key) is str else exit_on_key
self._merge_exit_key_binding()
self._default_bindings = load_key_bindings()
self._page_navigation_bindings = load_page_navigation_bindings()

Expand Down Expand Up @@ -339,6 +343,20 @@ def __init__(
# Trigger initialize callback.
self.reset()

def _merge_exit_key_binding(self) -> None:
if self.exit_on_key is None:
return

kb = KeyBindings()
kb.add(*self.exit_on_key)(handle_exit_on_key)

if self.key_bindings is None:
self.key_bindings = kb
else:
self.key_bindings = merge_key_bindings([kb, self.key_bindings])

return

def _create_merged_style(self, include_default_pygments_style: Filter) -> BaseStyle:
"""
Create a `Style` object that merges the default UI style, the default
Expand Down Expand Up @@ -1222,3 +1240,11 @@ def attach_winch_signal_handler(
previous_winch_handler._callback,
*previous_winch_handler._args,
)


def handle_exit_on_key(event: KeyPressEvent) -> None:
"""
Exit application function, to be used with a key binding
"""
event.app.exit()
return
96 changes: 96 additions & 0 deletions tests/test_application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import pytest

from prompt_toolkit.application import Application, get_app, handle_exit_on_key
from prompt_toolkit.application.current import set_app
from prompt_toolkit.key_binding.key_bindings import KeyBindings, _parse_key
from prompt_toolkit.keys import Keys
from test_key_binding import handlers, set_dummy_app


def test_exit_on_key_simple():
"""
Setting 'exit_on_key' should create a Binding in 'key_binding'
"""
with set_dummy_app(exit_on_key="c-c"):
exit_kb = KeyBindings()
exit_kb.add("c-c")(handle_exit_on_key)

app = get_app()

assert len(app.key_bindings.bindings) == 1

app_binding = app.key_bindings.bindings[0]
exit_binding = exit_kb.bindings[0]

assert app_binding.keys == exit_binding.keys
assert app_binding.handler == exit_binding.handler


def test_without_exit_on_key():
"""
Ommiting 'exit_on_key' should leave 'key_binding' unmodified
"""
kb = KeyBindings()

@kb.add("c-d")
def bind(e):
pass

with set_dummy_app(key_bindings=kb):
app = get_app()

assert app.exit_on_key is None
assert len(app.key_bindings.bindings) == 1

app_binding = app.key_bindings.bindings[0]

assert app_binding.keys == (_parse_key("c-d"),)
assert app_binding.handler == bind


def test_exit_on_key_not_override(handlers):
"""
'exit_on_key' should NOT override a 'key_bindings' Binding with same keys
"""
kb = KeyBindings()
kb.add("c-c")(handlers.control_c)

with set_dummy_app(exit_on_key="c-c", key_bindings=kb):
app = get_app()

assert len(app.key_bindings.bindings) == 2

app_last_binding = app.key_bindings.bindings[-1]
key_bindings_arg = kb.bindings[0]

# The last binding should be the one we passed in the key_bindings arg,
# not the one in exit_on_bind
assert app_last_binding.keys == key_bindings_arg.keys
assert app_last_binding.handler == key_bindings_arg.handler


def test_exit_on_key_mixed_bindings():
"""
'exit_on_key' should not interfere with other 'key_bindings' Bindings
"""
kb = KeyBindings()

@kb.add("c-d")
def bind_c_d(event):
pass

@kb.add("s-tab")
def bind_s_tab(event):
pass

with set_dummy_app(exit_on_key="c-c", key_bindings=kb):
app = get_app()

binding_tuples = [
(binding.keys, binding.handler) for binding in app.key_bindings.bindings
]

assert len(binding_tuples) == 3
assert ((_parse_key("c-c"),), handle_exit_on_key) in binding_tuples
assert ((_parse_key("c-d"),), bind_c_d) in binding_tuples
assert ((_parse_key("s-tab"),), bind_s_tab) in binding_tuples
7 changes: 5 additions & 2 deletions tests/test_key_binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@ def func(event):
return func


def set_dummy_app():
def set_dummy_app(**kwargs):
"""
Return a context manager that makes sure that this dummy application is
active. This is important, because we need an `Application` with
`is_done=False` flag, otherwise no keys will be processed.
"""
app = Application(
layout=Layout(Window()), output=DummyOutput(), input=create_pipe_input()
layout=Layout(Window()),
output=DummyOutput(),
input=create_pipe_input(),
**kwargs,
)
return set_app(app)

Expand Down