Skip to content

Commit e911f13

Browse files
Allow multiple REPLs on one page
Allow multiple REPLs on one page
2 parents d6d4ee5 + 4c0d7c8 commit e911f13

2 files changed

Lines changed: 70 additions & 25 deletions

File tree

src/embed.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,11 @@ declare global {
4444
var pyreplInfo: string;
4545
var pyreplStartupScript: string | undefined;
4646
var pyreplReadonly: boolean;
47-
var browserConsole: any;
47+
var currentBrowserConsole: any;
4848
}
4949

5050
let pyodidePromise: Promise<PyodideInterface> | null = null;
51+
let consoleCodePromise: Promise<string> | null = null;
5152

5253
let currentOutput: Terminal | null = null;
5354

@@ -191,6 +192,13 @@ function getPyodide(): Promise<PyodideInterface> {
191192
return pyodidePromise;
192193
}
193194

195+
function getConsoleCode(): Promise<string> {
196+
if (!consoleCodePromise) {
197+
consoleCodePromise = fetch("/python/console.py").then((r) => r.text());
198+
}
199+
return consoleCodePromise;
200+
}
201+
194202
function init() {
195203
if (document.readyState === "loading") {
196204
document.addEventListener("DOMContentLoaded", setup);
@@ -329,7 +337,8 @@ function createHeader(config: PyreplConfig): HTMLElement {
329337
return header;
330338
}
331339

332-
async function createRepl(container: HTMLElement) {
340+
// Create terminal UI without initializing Python (fast, shows background immediately)
341+
function createTerminal(container: HTMLElement): { term: Terminal; config: PyreplConfig } {
333342
injectStyles();
334343

335344
const config = parseConfig(container);
@@ -354,6 +363,10 @@ async function createRepl(container: HTMLElement) {
354363
});
355364
term.open(termContainer);
356365

366+
return { term, config };
367+
}
368+
369+
async function createRepl(container: HTMLElement, term: Terminal, config: PyreplConfig) {
357370
const pyodide = await getPyodide();
358371
await pyodide.loadPackage("micropip");
359372

@@ -389,13 +402,18 @@ async function createRepl(container: HTMLElement) {
389402
}
390403

391404
// Load and start the Python REPL
392-
const consoleCode = await fetch('/python/console.py').then(r => r.text());
405+
const consoleCode = await getConsoleCode();
393406
pyodide.runPython(consoleCode);
394-
pyodide.runPythonAsync('await start_repl()');
407+
pyodide.runPythonAsync("await start_repl()");
395408

396-
// Keep browserConsole reference alive for input handling
397-
const browserConsole = pyodide.globals.get('browser_console');
398-
globalThis.browserConsole = browserConsole;
409+
// Wait for Python to set currentBrowserConsole
410+
while (!(globalThis as any).currentBrowserConsole) {
411+
await new Promise((resolve) => setTimeout(resolve, 10));
412+
}
413+
const browserConsole = (globalThis as any).currentBrowserConsole;
414+
415+
// Clear it so next REPL can set its own
416+
(globalThis as any).currentBrowserConsole = null;
399417

400418
// Only attach input handler if not readonly
401419
if (!config.readonly) {
@@ -441,7 +459,16 @@ async function setup() {
441459
return;
442460
}
443461

444-
containers.forEach(createRepl);
462+
// Create all terminals first (fast, shows backgrounds immediately)
463+
const repls = Array.from(containers).map(container => ({
464+
container,
465+
...createTerminal(container)
466+
}));
467+
468+
// Then initialize Python REPLs sequentially (avoids race conditions)
469+
for (const { container, term, config } of repls) {
470+
await createRepl(container, term, config);
471+
}
445472
}
446473

447474
init();

src/python/console.py

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)