11"""IPython terminal interface using prompt_toolkit"""
22
33import os
4+ import signal
45import sys
56import inspect
67from warnings import warn
@@ -244,6 +245,15 @@ class TerminalInteractiveShell(InteractiveShell):
244245 """ ,
245246 ).tag (config = True )
246247
248+ background_prompt = Bool (
249+ False ,
250+ help = """Run prompt in background thread.
251+
252+ This allows typing input while code executes on the main thread.
253+ Some signal handling (Ctrl-C) may behave differently.
254+ """ ,
255+ ).tag (config = True )
256+
247257 @property
248258 def debugger_cls (self ):
249259 return Pdb if self .simple_prompt else TerminalPdb
@@ -802,6 +812,9 @@ def prompt():
802812 editing_mode = getattr (EditingMode , self .editing_mode .upper ())
803813
804814 self ._use_asyncio_inputhook = False
815+
816+ # Build extra kwargs for PromptSession
817+
805818 self .pt_app = PromptSession (
806819 auto_suggest = self .auto_suggest ,
807820 editing_mode = editing_mode ,
@@ -921,10 +934,38 @@ def get_message():
921934 ),
922935 ],
923936 }
937+ if self .background_prompt :
938+ # For background thread compatibility:
939+ # Poll terminal size instead of relying on SIGWINCH
940+ # (signals can't be handled in background threads)
941+ options ["refresh_interval" ] = 0.5
924942
925943 return options
926944
927945 def prompt_for_code (self ):
946+ if self ._use_asyncio_inputhook and False :
947+ asyncio_loop = get_asyncio_loop ()
948+ return asyncio_loop .run_until_complete (self ._prompt_for_code ())
949+ else :
950+ return next (self ._prompt_for_code ())
951+
952+ async def _prompt_for_code_async (self ):
953+ """Async version of prompt that can run in any event loop.
954+
955+ This is used by the background prompt thread to run the prompt
956+ in a separate event loop from the main thread.
957+ """
958+ if self .rl_next_input :
959+ default = self .rl_next_input
960+ self .rl_next_input = None
961+ else :
962+ default = ""
963+
964+ return await self .pt_app .prompt_async (
965+ default = default , ** self ._extra_prompt_options ()
966+ )
967+
968+ async def _prompt_for_code (self ):
928969 if self .rl_next_input :
929970 default = self .rl_next_input
930971 self .rl_next_input = None
@@ -942,20 +983,16 @@ def prompt_for_code(self):
942983 # When we integrate the asyncio event loop, run the UI in the
943984 # same event loop as the rest of the code. don't use an actual
944985 # input hook. (Asyncio is not made for nesting event loops.)
945- asyncio_loop = get_asyncio_loop ()
946- text = asyncio_loop .run_until_complete (
947- self .pt_app .prompt_async (
986+ return await self .pt_app .prompt_async (
948987 default = default , ** self ._extra_prompt_options ()
949988 )
950- )
951989 else :
952- text = self .pt_app .prompt (
990+ return self .pt_app .prompt (
953991 default = default ,
954992 inputhook = self ._inputhook ,
955993 ** self ._extra_prompt_options (),
956994 )
957995
958- return text
959996
960997 def init_io (self ):
961998 if sys .platform not in {'win32' , 'cli' }:
@@ -992,23 +1029,115 @@ def ask_exit(self):
9921029 self .keep_running = False
9931030
9941031 rl_next_input = None
1032+ _prompt_thread = None
1033+
1034+ @property
1035+ def pending_input_count (self ):
1036+ """Number of inputs waiting to be executed (background prompt mode only)."""
1037+ if self ._prompt_thread is not None :
1038+ return self ._prompt_thread .input_queue .qsize ()
1039+ return 0
9951040
9961041 def interact (self ):
1042+ """Main interaction loop with optional background prompt thread."""
9971043 self .keep_running = True
1044+
1045+ if self .background_prompt and not self .simple_prompt :
1046+ self ._interact_with_background_prompt ()
1047+ else :
1048+ self ._interact_sync ()
1049+
1050+ def _interact_sync (self ):
1051+ """Original synchronous interaction loop."""
9981052 while self .keep_running :
999- print (self .separate_in , end = '' )
1053+ print (self .separate_in , end = "" )
10001054
10011055 try :
10021056 code = self .prompt_for_code ()
10031057 except EOFError :
1004- if (not self .confirm_exit ) \
1005- or self .ask_yes_no ('Do you really want to exit ([y]/n)?' ,'y' ,'n' ):
1058+ if (not self .confirm_exit ) or self .ask_yes_no (
1059+ "Do you really want to exit ([y]/n)?" , "y" , "n"
1060+ ):
10061061 self .ask_exit ()
10071062
10081063 else :
10091064 if code :
10101065 self .run_cell (code , store_history = True )
10111066
1067+ def _interact_with_background_prompt (self ):
1068+ """Interaction loop using background prompt thread.
1069+
1070+ This allows users to type input while code executes on the main thread.
1071+ """
1072+ from .prompt_thread import (
1073+ InputPatcher ,
1074+ PromptThread ,
1075+ _EOFSentinel ,
1076+ _ExceptionSentinel ,
1077+ )
1078+
1079+ self ._executing = False
1080+ self ._flushed_count = 0
1081+
1082+ prompt_thread = PromptThread (self )
1083+
1084+ # SIGINT handler for during code execution
1085+ # We install this only during execution, not during prompting
1086+ def execution_sigint_handler (signum , frame ):
1087+ flushed = prompt_thread .flush_input_queue ()
1088+ if flushed > 0 :
1089+ self ._flushed_count = flushed
1090+ signal .default_int_handler (signum , frame )
1091+
1092+ prompt_thread .start ()
1093+
1094+ self ._prompt_thread = prompt_thread
1095+
1096+ # Patch builtins.input and wrap sys.stdin to route through the prompt thread
1097+ # This handles both input() calls and direct stdin access from user code
1098+ input_patcher = InputPatcher (prompt_thread )
1099+
1100+ try :
1101+ with input_patcher :
1102+ while self .keep_running :
1103+ print (self .separate_in , end = "" )
1104+
1105+ # Get input from background thread (blocks)
1106+ input_item = prompt_thread .get_input ()
1107+
1108+ if isinstance (input_item , _EOFSentinel ):
1109+ # Exit confirmation is handled in the prompt thread
1110+ # to avoid stdin conflicts
1111+ if input_item .should_exit :
1112+ self .ask_exit ()
1113+ # If should_exit is False, we continue the loop
1114+ # but the prompt thread already handles this case
1115+ elif isinstance (input_item , _ExceptionSentinel ):
1116+ raise input_item .exception
1117+ elif input_item :
1118+ # Execute code on main thread
1119+ # Install our SIGINT handler during execution only
1120+ # This avoids conflicts with prompt_toolkit's handling
1121+ prev_handler = signal .signal (
1122+ signal .SIGINT , execution_sigint_handler
1123+ )
1124+ self ._executing = True
1125+ self ._flushed_count = 0
1126+ try :
1127+ self .run_cell (input_item , store_history = True )
1128+ except KeyboardInterrupt :
1129+ msg = "\n KeyboardInterrupt"
1130+ if self ._flushed_count > 0 :
1131+ msg += f" ({ self ._flushed_count } pending input(s) discarded)"
1132+ print (msg )
1133+ finally :
1134+ self ._executing = False
1135+ signal .signal (signal .SIGINT , prev_handler )
1136+ finally :
1137+ self ._prompt_thread = None
1138+ prompt_thread .stop ()
1139+ prompt_thread .join (timeout = 2.0 )
1140+
10121141 def mainloop (self ):
10131142 # An extra layer of protection in case someone mashing Ctrl-C breaks
10141143 # out of our internal code.
0 commit comments