@@ -110,10 +110,19 @@ def repaint(self):
110110 pass
111111
112112
113- browser_console = BrowserConsole (js .term )
113+ async def start_repl ():
114+ # Create a new console for this terminal instance
115+ browser_console = BrowserConsole (js .term )
114116
117+ # Capture startup script before JS moves to next REPL and overwrites it
118+ startup_script = getattr (js , "pyreplStartupScript" , None )
119+ theme_name = getattr (js , "pyreplTheme" , "catppuccin-mocha" )
120+ info_line = getattr (js , "pyreplInfo" , "Python 3.13 (Pyodide)" )
121+ readonly = getattr (js , "pyreplReadonly" , False )
122+
123+ # Expose to JS so it can send input (signals JS can proceed to next REPL)
124+ js .currentBrowserConsole = browser_console
115125
116- async def start_repl ():
117126 import micropip
118127 import rlcompleter
119128 import re
@@ -125,7 +134,6 @@ async def start_repl():
125134 from pygments .formatters import Terminal256Formatter
126135
127136 lexer = Python3Lexer ()
128- theme_name = getattr (js , "pyreplTheme" , "catppuccin-mocha" )
129137 formatter = Terminal256Formatter (style = theme_name )
130138
131139 def syntax_highlight (code ):
@@ -141,20 +149,32 @@ def write(self, data):
141149 def flush (self ):
142150 pass
143151
144- sys .stdout = TermWriter ()
145- sys .stderr = TermWriter ()
152+ term_writer = TermWriter ()
153+
154+ # Custom exec that redirects stdout/stderr to this REPL's terminal
155+ import contextlib
146156
147- def displayhook (value ):
148- if value is not None :
149- repl_globals ["_" ] = value
150- browser_console .term .write (repr (value ) + "\r \n " )
157+ def exec_with_redirect (code , globals_dict ):
158+ old_displayhook = sys .displayhook
151159
152- sys .displayhook = displayhook
160+ def displayhook (value ):
161+ if value is not None :
162+ globals_dict ["_" ] = value
163+ browser_console .term .write (repr (value ) + "\r \n " )
164+
165+ sys .displayhook = displayhook
166+ try :
167+ with (
168+ contextlib .redirect_stdout (term_writer ),
169+ contextlib .redirect_stderr (term_writer ),
170+ ):
171+ exec (code , globals_dict )
172+ finally :
173+ sys .displayhook = old_displayhook
153174
154175 def clear ():
155176 browser_console .clear ()
156- info = getattr (js , "pyreplInfo" , "Python 3.13 (Pyodide)" )
157- browser_console .term .write (f"\x1b [90m{ info } \x1b [0m\r \n " )
177+ browser_console .term .write (f"\x1b [90m{ info_line } \x1b [0m\r \n " )
158178
159179 class Exit :
160180 def __repr__ (self ):
@@ -163,7 +183,6 @@ def __repr__(self):
163183 def __call__ (self ):
164184 browser_console .term .write ("exit is not available in the browser\r \n " )
165185
166- global repl_globals
167186 repl_globals = {
168187 "__builtins__" : __builtins__ ,
169188 "clear" : clear ,
@@ -173,7 +192,6 @@ def __call__(self):
173192 completer = rlcompleter .Completer (repl_globals )
174193
175194 # Run startup script if one was provided (silently, just to populate namespace)
176- startup_script = getattr (js , "pyreplStartupScript" , None )
177195 if startup_script :
178196 try :
179197 # Temporarily suppress stdout/stderr during startup
@@ -207,7 +225,7 @@ def get_word_to_complete(line):
207225 return match .group (0 ) if match else ""
208226
209227 # In readonly mode, don't show prompt or accept input
210- if getattr ( js , "pyreplReadonly" , False ) :
228+ if readonly :
211229 return
212230
213231 browser_console .term .write (PS1 )
@@ -302,7 +320,7 @@ def get_word_to_complete(line):
302320 source = "\n " .join (lines )
303321 try :
304322 code = compile (source , "<console>" , "single" )
305- exec (code , repl_globals )
323+ exec_with_redirect (code , repl_globals )
306324 history .append (source )
307325 history_index = len (history )
308326 except SystemExit :
@@ -332,7 +350,7 @@ def get_word_to_complete(line):
332350 history .append (source )
333351 history_index = len (history )
334352 try :
335- exec (code , repl_globals )
353+ exec_with_redirect (code , repl_globals )
336354 except SystemExit :
337355 pass
338356 except Exception as e :
0 commit comments