Skip to content

Instantly share code, notes, and snippets.

@snickell
Created May 30, 2024 03:22
Show Gist options
  • Save snickell/12cc254f74eb1d31914b22bf3a0e8cdd to your computer and use it in GitHub Desktop.
Save snickell/12cc254f74eb1d31914b22bf3a0e8cdd to your computer and use it in GitHub Desktop.
<!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("]]", "&rsqb;&rsqb;")
.replaceAll("[[", "&lsqb;&lsqb;"),
...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