Skip to content

Commit 12700a6

Browse files
Handle SIGINT (when sent from another process) and allow binding it to a key binding.
1 parent e9eac2e commit 12700a6

File tree

6 files changed

+36
-1
lines changed

6 files changed

+36
-1
lines changed

prompt_toolkit/application/application.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,7 @@ async def run_async(
630630
self,
631631
pre_run: Optional[Callable[[], None]] = None,
632632
set_exception_handler: bool = True,
633+
handle_sigint: bool = True,
633634
) -> _AppResult:
634635
"""
635636
Run the prompt_toolkit :class:`~prompt_toolkit.application.Application`
@@ -646,6 +647,9 @@ async def run_async(
646647
:param set_exception_handler: When set, in case of an exception, go out
647648
of the alternate screen and hide the application, display the
648649
exception, and wait for the user to press ENTER.
650+
:param handle_sigint: Handle SIGINT signal if possible. This will call
651+
the `<sigint>` key binding when a SIGINT is received. (This only
652+
works in the main thread.)
649653
"""
650654
assert not self._is_running, "Application is already running."
651655

@@ -781,6 +785,15 @@ async def _run_async2() -> _AppResult:
781785
self._invalidated = False
782786

783787
loop = get_event_loop()
788+
789+
if handle_sigint:
790+
loop.add_signal_handler(
791+
signal.SIGINT,
792+
lambda *_: loop.call_soon_threadsafe(
793+
self.key_processor.send_sigint
794+
),
795+
)
796+
784797
if set_exception_handler:
785798
previous_exc_handler = loop.get_exception_handler()
786799
loop.set_exception_handler(self._handle_exception)
@@ -812,12 +825,16 @@ async def _run_async2() -> _AppResult:
812825
if set_exception_handler:
813826
loop.set_exception_handler(previous_exc_handler)
814827

828+
if handle_sigint:
829+
loop.remove_signal_handler(signal.SIGINT)
830+
815831
return await _run_async2()
816832

817833
def run(
818834
self,
819835
pre_run: Optional[Callable[[], None]] = None,
820836
set_exception_handler: bool = True,
837+
handle_sigint: bool = True,
821838
in_thread: bool = False,
822839
) -> _AppResult:
823840
"""
@@ -843,6 +860,8 @@ def run(
843860
`get_appp().create_background_task()`, so that unfinished tasks are
844861
properly cancelled before the event loop is closed. This is used
845862
for instance in ptpython.
863+
:param handle_sigint: Handle SIGINT signal. Call the key binding for
864+
`Keys.SIGINT`. (This only works in the main thread.)
846865
"""
847866
if in_thread:
848867
result: _AppResult
@@ -852,7 +871,10 @@ def run_in_thread() -> None:
852871
nonlocal result, exception
853872
try:
854873
result = self.run(
855-
pre_run=pre_run, set_exception_handler=set_exception_handler
874+
pre_run=pre_run,
875+
set_exception_handler=set_exception_handler,
876+
# Signal handling only works in the main thread.
877+
handle_sigint=False,
856878
)
857879
except BaseException as e:
858880
exception = e

prompt_toolkit/application/dummy.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def run(
2424
self,
2525
pre_run: Optional[Callable[[], None]] = None,
2626
set_exception_handler: bool = True,
27+
handle_sigint: bool = True,
2728
in_thread: bool = False,
2829
) -> None:
2930
raise NotImplementedError("A DummyApplication is not supposed to run.")
@@ -32,6 +33,7 @@ async def run_async(
3233
self,
3334
pre_run: Optional[Callable[[], None]] = None,
3435
set_exception_handler: bool = True,
36+
handle_sigint: bool = True,
3537
) -> None:
3638
raise NotImplementedError("A DummyApplication is not supposed to run.")
3739

prompt_toolkit/key_binding/bindings/basic.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ def load_basic_bindings() -> KeyBindings:
120120
@handle("insert")
121121
@handle("s-insert")
122122
@handle("c-insert")
123+
@handle("<sigint>")
123124
@handle(Keys.Ignore)
124125
def _ignore(event: E) -> None:
125126
"""

prompt_toolkit/key_binding/key_processor.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,13 @@ def flush_keys() -> None:
411411
self._flush_wait_task.cancel()
412412
self._flush_wait_task = app.create_background_task(wait())
413413

414+
def send_sigint(self) -> None:
415+
"""
416+
Send SIGINT. Immediately call the SIGINT key handler.
417+
"""
418+
self.feed(KeyPress(key=Keys.SIGINT), first=True)
419+
self.process_keys()
420+
414421

415422
class KeyPressEvent:
416423
"""

prompt_toolkit/keys.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,8 @@ class Keys(str, Enum):
184184
WindowsMouseEvent = "<windows-mouse-event>"
185185
BracketedPaste = "<bracketed-paste>"
186186

187+
SIGINT = "<sigint>"
188+
187189
# For internal use: key which is ignored.
188190
# (The key binding for this key should not do anything.)
189191
Ignore = "<ignore>"

prompt_toolkit/shortcuts/prompt.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,7 @@ def _complete_like_readline(event: E) -> None:
807807
display_completions_like_readline(event)
808808

809809
@handle("c-c", filter=default_focused)
810+
@handle("<sigint>")
810811
def _keyboard_interrupt(event: E) -> None:
811812
"Abort when Control-C has been pressed."
812813
event.app.exit(exception=KeyboardInterrupt, style="class:aborting")

0 commit comments

Comments
 (0)