Skip to content
Merged
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
22 changes: 22 additions & 0 deletions docs/pages/advanced_topics/key_bindings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,28 @@ possible. Check the `custom-vi-operator-and-text-object.py` example for more
information.


Handling SIGINT
---------------

The SIGINT Unix signal can be handled by binding ``<sigint>``. For instance:

.. code:: python

@bindings.add('<sigint>')
def _(event):
# ...
pass

This will handle a SIGINT that was sent by an external application into the
process. Handling control-c should be done by binding ``c-c``. (The terminal
input is set to raw mode, which means that a ``c-c`` won't be translated into a
SIGINT.)

For a ``PromptSession``, there is a default binding for ``<sigint>`` that
corresponds to ``c-c``: it will exit the prompt, raising a
``KeyboardInterrupt`` exception.


Processing `.inputrc`
---------------------

Expand Down
28 changes: 27 additions & 1 deletion prompt_toolkit/application/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,7 @@ async def run_async(
self,
pre_run: Optional[Callable[[], None]] = None,
set_exception_handler: bool = True,
handle_sigint: bool = True,
) -> _AppResult:
"""
Run the prompt_toolkit :class:`~prompt_toolkit.application.Application`
Expand All @@ -646,9 +647,16 @@ async def run_async(
: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.)
"""
assert not self._is_running, "Application is already running."

if not in_main_thread():
# Handling signals in other threads is not supported.
handle_sigint = False

async def _run_async() -> _AppResult:
"Coroutine."
loop = get_event_loop()
Expand Down Expand Up @@ -781,6 +789,15 @@ async def _run_async2() -> _AppResult:
self._invalidated = False

loop = get_event_loop()

if handle_sigint:
loop.add_signal_handler(
signal.SIGINT,
lambda *_: loop.call_soon_threadsafe(
self.key_processor.send_sigint
),
)

if set_exception_handler:
previous_exc_handler = loop.get_exception_handler()
loop.set_exception_handler(self._handle_exception)
Expand Down Expand Up @@ -812,12 +829,16 @@ async def _run_async2() -> _AppResult:
if set_exception_handler:
loop.set_exception_handler(previous_exc_handler)

if handle_sigint:
loop.remove_signal_handler(signal.SIGINT)

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:
"""
Expand All @@ -843,6 +864,8 @@ def run(
`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
Expand All @@ -852,7 +875,10 @@ def run_in_thread() -> None:
nonlocal result, exception
try:
result = self.run(
pre_run=pre_run, set_exception_handler=set_exception_handler
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
Expand Down
2 changes: 2 additions & 0 deletions prompt_toolkit/application/dummy.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ 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.")
Expand All @@ -32,6 +33,7 @@ async def run_async(
self,
pre_run: Optional[Callable[[], None]] = None,
set_exception_handler: bool = True,
handle_sigint: bool = True,
) -> None:
raise NotImplementedError("A DummyApplication is not supposed to run.")

Expand Down
1 change: 1 addition & 0 deletions prompt_toolkit/key_binding/bindings/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ def load_basic_bindings() -> KeyBindings:
@handle("insert")
@handle("s-insert")
@handle("c-insert")
@handle("<sigint>")
@handle(Keys.Ignore)
def _ignore(event: E) -> None:
"""
Expand Down
7 changes: 7 additions & 0 deletions prompt_toolkit/key_binding/key_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,13 @@ def flush_keys() -> None:
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:
"""
Expand Down
2 changes: 2 additions & 0 deletions prompt_toolkit/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ class Keys(str, Enum):
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>"
Expand Down
1 change: 1 addition & 0 deletions prompt_toolkit/shortcuts/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,7 @@ def _complete_like_readline(event: E) -> None:
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")
Expand Down