UNPKG

pyodide

Version:

The Pyodide JavaScript package

394 lines (363 loc) 13.3 kB
<!DOCTYPE html> <!-- Pyodide Console v2 (Experimental) This is an experimental version of the Pyodide console that provides an enhanced terminal experience using xterm.js. This implementation replaces the jQuery Terminal used in the original console with a more feature-rich terminal emulator that offers better performance and modern terminal capabilities. Note: This console is still under development and may not have all the features of the stable console. --> <html> <head> <title>Pyodide Console</title> <meta charset="UTF-8" /> <meta http-equiv="origin-trial" content="Aq6vv/4syIkcyMszFgCc9LlH0kX88jdE7SXfCFnh2RQN0nhhL8o6PCQ2oE3a7n3mC7+d9n89Repw5HYBtjarDw4AAAB3eyJvcmlnaW4iOiJodHRwczovL3B5b2RpZGUub3JnOjQ0MyIsImZlYXR1cmUiOiJXZWJBc3NlbWJseUpTUHJvbWlzZUludGVncmF0aW9uIiwiZXhwaXJ5IjoxNzMwMjQ2Mzk5LCJpc1N1YmRvbWFpbiI6dHJ1ZX0=" /> <meta http-equiv="origin-trial" content="Ai8IXb0XqedlM/Q2guWXFfBkKiYY9uaPZpdjHqc8y0ZvpAfK9SKzp/dIuFH+txG/HEKxt59uIkk39hhWrhNgbw4AAABieyJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjgwMDAiLCJmZWF0dXJlIjoiV2ViQXNzZW1ibHlKU1Byb21pc2VJbnRlZ3JhdGlvbiIsImV4cGlyeSI6MTczMDI0NjM5OX0=" /> <link rel="stylesheet" href="https://unpkg.com/@xterm/xterm@5.4.0/css/xterm.css" /> <link href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🐍</text></svg>" rel="icon" /> <script src="https://unpkg.com/@xterm/xterm@5.4.0/lib/xterm.js"></script> <script src="https://unpkg.com/@xterm/addon-fit@0.9.0/lib/addon-fit.js"></script> <style> * { margin: 0; padding: 0; box-sizing: border-box; } html, body { margin: 0; background-color: #000000; font-family: "Monaco", "Menlo", "Courier New", monospace; overflow: hidden; } #terminal { position: fixed; inset: 10px; } #loading { display: inline-block; width: 50px; height: 50px; position: fixed; top: 50%; left: 50%; border: 3px solid rgba(172, 237, 255, 0.5); border-radius: 50%; border-top-color: #fff; animation: spin 1s ease-in-out infinite; -webkit-animation: spin 1s ease-in-out infinite; } @keyframes spin { to { -webkit-transform: rotate(360deg); } } @-webkit-keyframes spin { to { -webkit-transform: rotate(360deg); } } </style> </head> <body> <div id="loading"></div> <div id="terminal"></div> <script type="module"> async function main() { const fitAddon = new FitAddon.FitAddon(); const term = new Terminal({ cursorBlink: true, cursorStyle: "block", convertEol: true, scrollback: 2_000, fontSize: 18, lineHeight: 1.4, fontFamily: "monospace", theme: { background: "#000000", foreground: "rgba(255, 255, 255, 0.8)", cursor: "rgba(255, 255, 255, 0.8)", selection: "#404040", error: "#ff0000", }, }); window.term = term; term.open(document.getElementById("terminal")); term.loadAddon(fitAddon); fitAddon.fit(); term.focus(); window.addEventListener("resize", () => { setTimeout(() => fitAddon.fit(), 50); }); // Re-fit after the page has fully loaded window.addEventListener("load", () => { setTimeout(() => fitAddon.fit(), 100); }); // Initialize Pyodide let indexURL = "./"; const urlParams = new URLSearchParams(window.location.search); const buildParam = urlParams.get("build"); if (buildParam && ["full", "debug", "pyc"].includes(buildParam)) { indexURL = indexURL.replace("/full/", "/" + buildParam + "/"); } const { loadPyodide } = await import(indexURL + "pyodide.mjs"); const pyodide = await loadPyodide(); globalThis.pyodide = pyodide; // Hide loading spinner document.getElementById("loading").style.display = "none"; const { repr_shorten, BANNER, PyodideConsole } = pyodide.pyimport("pyodide.console"); term.writeln( `Welcome to the Pyodide ${pyodide.version} terminal emulator 🐍\n${BANNER}` ); const pyconsole = PyodideConsole(pyodide.globals); const namespace = pyodide.globals.get("dict")(); const await_fut = pyodide.runPython( ` import builtins from pyodide.ffi import to_js async def await_fut(fut): res = await fut if res is not None: builtins._ = res return to_js([res], depth=1) await_fut `, { globals: namespace } ); namespace.destroy(); pyconsole.stdout_callback = (s) => term.write(s); pyconsole.stderr_callback = (s) => term.write(`\x1b[31m${s}\x1b[0m`); // Handle fatal errors pyodide._api.on_fatal = async (e) => { if (e.name === "Exit") { term.write(`\x1b[31m${e}\x1b[0m\r\n`); term.write( "\x1b[31mPyodide exited and can no longer be used.\x1b[0m\r\n" ); } else { term.write( "\x1b[31mPyodide has suffered a fatal error. Please report this to the Pyodide maintainers.\x1b[0m\r\n" ); term.write("\x1b[31mThe cause of the fatal error was:\x1b[0m\r\n"); term.write(`\x1b[31m${e.message || e}\x1b[0m\r\n`); term.write( "\x1b[31mLook in the browser console for more details.\x1b[0m\r\n" ); } }; // REPL implementation const ps1 = ">>> "; const ps2 = "... "; let buffer = ""; let cursorIndex = 0; // index within buffer for in-line editing let prompt = ps1; const history = []; let historyIndex = null; // null means not navigating history term.write(prompt); function addToHistory(command) { const trimmed = command.trimEnd(); if (!trimmed) return; const last = history[history.length - 1]; if (last !== trimmed) history.push(trimmed); } function refreshLine() { // Write left part, save cursor, write right part, clear, restore cursor. const clearCommand = "\x1b[0K"; const leftPart = prompt + buffer.slice(0, cursorIndex); const rightPart = buffer.slice(cursorIndex); term.write( `\x1b[0G${leftPart}\x1b[s${rightPart}${clearCommand}\x1b[u` ); } function setBuffer(newBuffer, newCursorIndex = null) { buffer = newBuffer; if (newCursorIndex === null) { cursorIndex = buffer.length; } else { cursorIndex = Math.max(0, Math.min(newCursorIndex, buffer.length)); } refreshLine(); } async function execLine(line) { // Normalize non-breaking spaces to regular spaces line = line.replace(/\u00a0/g, " "); // clear the terminal if (line === "clear") { term.clear(); return; } const fut = pyconsole.push(line); switch (fut.syntax_check) { case "syntax-error": term.write(`\x1b[31m${fut.formatted_error.trimEnd()}\x1b[0m`); term.write("\r\n"); prompt = ps1; addToHistory(line); historyIndex = null; fut.destroy(); break; case "incomplete": prompt = ps2; addToHistory(line); historyIndex = null; return; case "complete": prompt = ps1; try { const wrapped = await_fut(fut); const [value] = await wrapped; if (value !== undefined) { const output = repr_shorten.callKwargs(value, { separator: "\n<long output truncated>\n", }); term.write(output); term.write("\r\n"); } if (value instanceof pyodide.ffi.PyProxy) value.destroy(); wrapped.destroy(); } catch (e) { const msg = fut.formatted_error || e.message; term.write(`\x1b[31m${String(msg).trimEnd()}\x1b[0m`); term.write("\r\n"); } finally { fut.destroy(); } addToHistory(line); historyIndex = null; break; default: term.write( `\r\nUnexpected syntax_check value: ${fut.syntax_check}` ); } } term.onData(async (data) => { switch (data) { case "\r": // Enter term.write("\r\n"); await execLine(buffer); buffer = ""; cursorIndex = 0; term.write(prompt); break; case "\u0003": // Ctrl-C pyconsole.buffer.clear(); buffer = ""; cursorIndex = 0; term.write("^C\r\nKeyboardInterrupt\r\n" + ps1); prompt = ps1; historyIndex = null; break; case "\u0016": // Ctrl-V // paste from clipboard const clipboard = await navigator.clipboard.readText(); const newBuf = buffer.slice(0, cursorIndex) + clipboard + buffer.slice(cursorIndex); setBuffer(newBuf, newBuf.length); break; case "\u007F": // Backspace if (cursorIndex > 0) { const before = buffer.slice(0, cursorIndex - 1); const after = buffer.slice(cursorIndex); cursorIndex -= 1; setBuffer(before + after, cursorIndex); } break; case "\x1B[A": // Up arrow if (prompt === ps1) { if (historyIndex === null) historyIndex = history.length; if (historyIndex > 0) { historyIndex -= 1; const newBuf = history[historyIndex] || ""; setBuffer(newBuf, newBuf.length); } } break; case "\x1B[B": // Down arrow if (prompt === ps1 && historyIndex !== null) { if (historyIndex < history.length - 1) { historyIndex += 1; const newBuf = history[historyIndex] || ""; setBuffer(newBuf, newBuf.length); } else { historyIndex = null; setBuffer("", 0); } } break; case "\x1B[C": // Right arrow if (cursorIndex < buffer.length) { cursorIndex += 1; refreshLine(); } break; case "\x1B[D": // Left arrow if (cursorIndex > 0) { cursorIndex -= 1; refreshLine(); } break; default: if (data) { // Normalize non-breaking spaces to regular spaces data = data.replace(/\u00a0/g, " "); // Insert arbitrary string at cursor position const before = buffer.slice(0, cursorIndex); const after = buffer.slice(cursorIndex); const newBuf = before + data + after; const newCursor = cursorIndex + data.length; setBuffer(newBuf, newCursor); } } }); // 4. Extra features let idbkvPromise; async function getIDBKV() { if (!idbkvPromise) { idbkvPromise = await import( "https://unpkg.com/idb-keyval@5.0.2/dist/esm/index.js" ); } return idbkvPromise; } async function mountDirectory(pyodideDirectory, directoryKey) { if (pyodide.FS.analyzePath(pyodideDirectory).exists) { return; } const { get, set } = await getIDBKV(); const opts = { id: "mountdirid", mode: "readwrite" }; let directoryHandle = await get(directoryKey); if (!directoryHandle) { directoryHandle = await showDirectoryPicker(opts); await set(directoryKey, directoryHandle); } const permissionStatus = await directoryHandle.requestPermission( opts ); if (permissionStatus !== "granted") { throw new Error("readwrite access to directory not granted"); } await pyodide.mountNativeFS(pyodideDirectory, directoryHandle); } globalThis.mountDirectory = mountDirectory; } window.console_ready = main(); </script> </body> </html>