|
<!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" |
|
/> |
|
<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> |
|
|