| <!doctype html> |
| <html> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta |
| http-equiv="origin-trial" |
| content="Aq6vv/4syIkcyMszFgCc9LlH0kX88jdE7SXfCFnh2RQN0nhhL8o6PCQ2oE3a7n3mC7+d9n89Repw5HYBtjarDw4AAAB3eyJvcmlnaW4iOiJodHRwczovL3B5b2RpZGUub3JnOjQ0MyIsImZlYXR1cmUiOiJXZWJBc3NlbWJseUpTUHJvbWlzZUludGVncmF0aW9uIiwiZXhwaXJ5IjoxNzMwMjQ2Mzk5LCJpc1N1YmRvbWFpbiI6dHJ1ZX0=" |
| /> |
| <meta |
| http-equiv="origin-trial" |
| content="Ai8IXb0XqedlM/Q2guWXFfBkKiYY9uaPZpdjHqc8y0ZvpAfK9SKzp/dIuFH+txG/HEKxt59uIkk39hhWrhNgbw4AAABieyJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjgwMDAiLCJmZWF0dXJlIjoiV2ViQXNzZW1ibHlKU1Byb21pc2VJbnRlZ3JhdGlvbiIsImV4cGlyeSI6MTczMDI0NjM5OX0=" |
| /> |
| <script src="https://cdn.jsdelivr.net/npm/jquery"></script> |
| <script src="https://cdn.jsdelivr.net/npm/jquery.terminal@2.35.2/js/jquery.terminal.min.js"></script> |
| <script src="https://cdn.jsdelivr.net/npm/jquery.terminal@2.35.2/js/unix_formatting.min.js"></script> |
| <link |
| href="https://cdn.jsdelivr.net/npm/jquery.terminal@2.35.2/css/jquery.terminal.min.css" |
| rel="stylesheet" |
| /> |
| <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" |
| /> |
| <style> |
| .terminal { |
| --size: 1.5; |
| --color: rgba(255, 255, 255, 0.8); |
| } |
| .noblink { |
| --animation: terminal-none; |
| } |
| body { |
| background-color: black; |
| } |
| #jquery-terminal-logo { |
| color: white; |
| border-color: white; |
| position: absolute; |
| top: 7px; |
| right: 18px; |
| z-index: 2; |
| } |
| #jquery-terminal-logo a { |
| color: gray; |
| text-decoration: none; |
| font-size: 0.7em; |
| } |
| #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="jquery-terminal-logo"> |
| <a href="https://terminal.jcubic.pl/">jQuery Terminal</a> |
| </div> |
| <div id="loading"></div> |
| <script> |
| "use strict"; |
| |
| function sleep(s) { |
| return new Promise((resolve) => setTimeout(resolve, s)); |
| } |
| |
| async function main() { |
| let indexURL = "./"; |
| const urlParams = new URLSearchParams(window.location.search); |
| const buildParam = urlParams.get("build"); |
| if (buildParam) { |
| if (["full", "debug", "pyc"].includes(buildParam)) { |
| indexURL = indexURL.replace( |
| "/full/", |
| "/" + urlParams.get("build") + "/", |
| ); |
| } else { |
| console.warn( |
| 'Invalid URL parameter: build="' + |
| buildParam + |
| '". Using default "full".', |
| ); |
| } |
| } |
| const { loadPyodide } = await import(indexURL + "pyodide.mjs"); |
| |
| globalThis.loadPyodide = loadPyodide; |
| |
| let term; |
| globalThis.pyodide = await loadPyodide({ |
| stdin: () => { |
| let result = prompt(); |
| echo(result); |
| return result; |
| }, |
| }); |
| let { repr_shorten, BANNER, PyodideConsole } = |
| pyodide.pyimport("pyodide.console"); |
| BANNER = |
| `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(); |
| |
| const echo = (msg, ...opts) => |
| term.echo( |
| msg |
| .replaceAll("]]", "]]") |
| .replaceAll("[[", "[["), |
| ...opts, |
| ); |
| |
| const ps1 = ">>> "; |
| const ps2 = "... "; |
| |
| async function lock() { |
| let resolve; |
| const ready = term.ready; |
| term.ready = new Promise((res) => (resolve = res)); |
| await ready; |
| return resolve; |
| } |
| |
| async function interpreter(command) { |
| const unlock = await lock(); |
| term.pause(); |
| |
| for (const c of command.split("\n")) { |
| const escaped = c.replaceAll(/\u00a0/g, " "); |
| const fut = pyconsole.push(escaped); |
| term.set_prompt(fut.syntax_check === "incomplete" ? ps2 : ps1); |
| switch (fut.syntax_check) { |
| case "syntax-error": |
| term.error(fut.formatted_error.trimEnd()); |
| continue; |
| case "incomplete": |
| continue; |
| case "complete": |
| break; |
| default: |
| throw new Error(`Unexpected type ${ty}`); |
| } |
| |
| |
| |
| |
| const wrapped = await_fut(fut); |
| |
| try { |
| const [value] = await wrapped; |
| if (value !== undefined) { |
| echo( |
| repr_shorten.callKwargs(value, { |
| separator: "\n<long output truncated>\n", |
| }), |
| ); |
| } |
| if (value instanceof pyodide.ffi.PyProxy) { |
| value.destroy(); |
| } |
| } catch (e) { |
| if (e.constructor.name === "PythonError") { |
| const message = fut.formatted_error || e.message; |
| term.error(message.trimEnd()); |
| } else { |
| throw e; |
| } |
| } finally { |
| fut.destroy(); |
| wrapped.destroy(); |
| } |
| } |
| term.resume(); |
| await sleep(10); |
| unlock(); |
| } |
| |
| term = $("body").terminal(interpreter, { |
| greetings: BANNER, |
| prompt: ps1, |
| completionEscape: false, |
| completion: function (command, callback) { |
| callback(pyconsole.complete(command).toJs()[0]); |
| }, |
| keymap: { |
| "CTRL+C": async function (event, original) { |
| pyconsole.buffer.clear(); |
| term.enter(); |
| echo("KeyboardInterrupt"); |
| term.set_command(""); |
| term.set_prompt(ps1); |
| }, |
| TAB: (event, original) => { |
| const command = term.before_cursor(); |
| |
| if (command.trim() === "") { |
| term.insert("\t"); |
| return false; |
| } |
| return original(event); |
| }, |
| }, |
| }); |
| window.term = term; |
| pyconsole.stdout_callback = (s) => echo(s, { newline: false }); |
| pyconsole.stderr_callback = (s) => { |
| term.error(s.trimEnd()); |
| }; |
| term.ready = Promise.resolve(); |
| pyodide._api.on_fatal = async (e) => { |
| if (e.name === "Exit") { |
| term.error(e); |
| term.error("Pyodide exited and can no longer be used."); |
| } else { |
| term.error( |
| "Pyodide has suffered a fatal error. Please report this to the Pyodide maintainers.", |
| ); |
| term.error("The cause of the fatal error was:"); |
| term.error(e); |
| term.error("Look in the browser console for more details."); |
| } |
| await term.ready; |
| term.pause(); |
| await sleep(15); |
| term.pause(); |
| }; |
| |
| const searchParams = new URLSearchParams(window.location.search); |
| if (searchParams.has("noblink")) { |
| $(".cmd-cursor").addClass("noblink"); |
| } |
| |
| 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> |
|
|