Created
May 30, 2024 03:22
-
-
Save snickell/12cc254f74eb1d31914b22bf3a0e8cdd to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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/[email protected]/js/jquery.terminal.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/js/unix_formatting.min.js"></script> | |
<link | |
href="https://cdn.jsdelivr.net/npm/[email protected]/css/jquery.terminal.min.css" | |
rel="stylesheet" | |
/> | |
<style> | |
.terminal { | |
--size: 1.5; | |
--color: rgba(255, 255, 255, 0.8); | |
} | |
#terms .terminal:nth-child(1) { | |
background-color: purple; | |
} | |
#terms .terminal:nth-child(2) { | |
background-color: blue; | |
} | |
#terms .terminal:nth-child(3) { | |
background-color: brown; | |
} | |
.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); | |
} | |
} | |
<script defer data-domain="pyodide.org" src="https://plausible.io/js/plausible.js"></script> | |
</style> | |
</head> | |
<body> | |
<div id="jquery-terminal-logo"> | |
<a href="https://terminal.jcubic.pl/">jQuery Terminal</a> | |
</div> | |
<div id="terms"> | |
</div> | |
<script> | |
"use strict"; | |
function sleep(s) { | |
return new Promise((resolve) => setTimeout(resolve, s)); | |
} | |
async function startNewPyConsole() { | |
let indexURL = "https://cdn.jsdelivr.net/pyodide/v0.26.0/full/"; | |
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"); | |
// to facilitate debugging | |
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(); | |
// multiline should be split (useful when pasting) | |
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}`); | |
} | |
// In JavaScript, await automatically also awaits any results of | |
// awaits, so if an async function returns a future, it will await | |
// the inner future too. This is not what we want so we | |
// temporarily put it into a list to protect it. | |
const wrapped = await_fut(fut); | |
// complete case, get result / error and print it. | |
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(); | |
} | |
let newDiv = $('<div/>'); | |
newDiv.appendTo('#terms'); | |
term = newDiv.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(); | |
// Disable completion for whitespaces. | |
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/[email protected]/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; | |
} | |
startNewPyConsole(); | |
startNewPyConsole(); | |
startNewPyConsole(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment