mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-20 02:37:41 -05:00
* Fix #1998 - Allow lazy PyTerminal bootstrap * Fix #1998 - Allow lazy terminal bootstrap / runtime * Implemented mpy terminal in both main and worker * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
254 lines
8.9 KiB
JavaScript
254 lines
8.9 KiB
JavaScript
// PyScript py-terminal plugin
|
|
import { TYPES, hooks } from "../core.js";
|
|
import { notify } from "./error.js";
|
|
import { customObserver, defineProperty } from "polyscript/exports";
|
|
|
|
// will contain all valid selectors
|
|
const SELECTORS = [];
|
|
|
|
// show the error on main and
|
|
// stops the module from keep executing
|
|
const notifyAndThrow = (message) => {
|
|
notify(message);
|
|
throw new Error(message);
|
|
};
|
|
|
|
const onceOnMain = ({ attributes: { worker } }) => !worker;
|
|
|
|
const bootstrapped = new WeakSet();
|
|
|
|
let addStyle = true;
|
|
|
|
// this callback will be serialized as string and it never needs
|
|
// to be invoked multiple times. Each xworker here is bootstrapped
|
|
// only once thanks to the `sync.is_pyterminal()` check.
|
|
const workerReady = ({ interpreter, io, run, type }, { sync }) => {
|
|
if (!sync.is_pyterminal()) return;
|
|
|
|
// in workers it's always safe to grab the polyscript currentScript
|
|
// the ugly `_` dance is due MicroPython not able to import via:
|
|
// `from polyscript.currentScript import terminal as __terminal__`
|
|
run(
|
|
"from polyscript import currentScript as _; __terminal__ = _.terminal; del _",
|
|
);
|
|
|
|
// This part is shared among both Pyodide and MicroPython
|
|
io.stderr = (error) => {
|
|
sync.pyterminal_write(`${error.message || error}\n`);
|
|
};
|
|
|
|
const isMicroPython = type === "mpy";
|
|
|
|
// MicroPython has no code or code.interact()
|
|
// This part patches it in a way that simulate
|
|
// the code.interact() module in Pyodide.
|
|
if (isMicroPython) {
|
|
const encoder = new TextEncoder();
|
|
const processData = () => {
|
|
if (data.length) {
|
|
for (
|
|
let i = 0, b = encoder.encode(`${data}\r`);
|
|
i < b.length;
|
|
i++
|
|
) {
|
|
const code = interpreter.replProcessChar(b[i]);
|
|
if (code) {
|
|
throw new Error(
|
|
`replProcessChar failed with code ${code}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
data = ">>> ";
|
|
data = io.stdin();
|
|
processData();
|
|
};
|
|
interpreter.setStderr = Object; // as no-op
|
|
interpreter.setStdout = ({ write }) => {
|
|
io.stdout = (str) => {
|
|
// avoid duplicated outcome due i/o + readline
|
|
const ignore = str.startsWith(`>>> ${data}`);
|
|
return ignore ? 0 : write(`${str}\n`);
|
|
};
|
|
};
|
|
interpreter.setStdin = ({ stdin }) => {
|
|
io.stdin = stdin;
|
|
};
|
|
// tiny shim of the code module with only interact
|
|
// to bootstrap a REPL like environment
|
|
interpreter.registerJsModule("code", {
|
|
interact() {
|
|
interpreter.replInit();
|
|
data = "";
|
|
processData();
|
|
},
|
|
});
|
|
}
|
|
|
|
// This part is inevitably duplicated as external scope
|
|
// can't be reached by workers out of the box.
|
|
// The detail is that here we use sync though, not readline.
|
|
const decoder = new TextDecoder();
|
|
let data = "";
|
|
const generic = {
|
|
isatty: true,
|
|
write(buffer) {
|
|
data = isMicroPython ? buffer : decoder.decode(buffer);
|
|
sync.pyterminal_write(data);
|
|
return buffer.length;
|
|
},
|
|
};
|
|
interpreter.setStdout(generic);
|
|
interpreter.setStderr(generic);
|
|
interpreter.setStdin({
|
|
isatty: true,
|
|
stdin: () => sync.pyterminal_read(data),
|
|
});
|
|
};
|
|
|
|
const pyTerminal = async (element) => {
|
|
// lazy load these only when a valid terminal is found
|
|
const [{ Terminal }, { Readline }, { FitAddon }] = await Promise.all([
|
|
import(/* webpackIgnore: true */ "../3rd-party/xterm.js"),
|
|
import(/* webpackIgnore: true */ "../3rd-party/xterm-readline.js"),
|
|
import(/* webpackIgnore: true */ "../3rd-party/xterm_addon-fit.js"),
|
|
]);
|
|
|
|
const readline = new Readline();
|
|
|
|
// common main thread initialization for both worker
|
|
// or main case, bootstrapping the terminal on its target
|
|
const init = (options) => {
|
|
let target = element;
|
|
const selector = element.getAttribute("target");
|
|
if (selector) {
|
|
target =
|
|
document.getElementById(selector) ||
|
|
document.querySelector(selector);
|
|
if (!target) throw new Error(`Unknown target ${selector}`);
|
|
} else {
|
|
target = document.createElement("py-terminal");
|
|
target.style.display = "block";
|
|
element.after(target);
|
|
}
|
|
const terminal = new Terminal({
|
|
theme: {
|
|
background: "#191A19",
|
|
foreground: "#F5F2E7",
|
|
},
|
|
...options,
|
|
});
|
|
const fitAddon = new FitAddon();
|
|
terminal.loadAddon(fitAddon);
|
|
terminal.loadAddon(readline);
|
|
terminal.open(target);
|
|
fitAddon.fit();
|
|
terminal.focus();
|
|
defineProperty(element, "terminal", { value: terminal });
|
|
return terminal;
|
|
};
|
|
|
|
// branch logic for the worker
|
|
if (element.hasAttribute("worker")) {
|
|
// add a hook on the main thread to setup all sync helpers
|
|
// also bootstrapping the XTerm target on main *BUT* ...
|
|
hooks.main.onWorker.add(function worker(_, xworker) {
|
|
// ... as multiple workers will add multiple callbacks
|
|
// be sure no xworker is ever initialized twice!
|
|
if (bootstrapped.has(xworker)) return;
|
|
bootstrapped.add(xworker);
|
|
|
|
// still cleanup this callback for future scripts/workers
|
|
hooks.main.onWorker.delete(worker);
|
|
|
|
init({
|
|
disableStdin: false,
|
|
cursorBlink: true,
|
|
cursorStyle: "block",
|
|
});
|
|
|
|
xworker.sync.is_pyterminal = () => true;
|
|
xworker.sync.pyterminal_read = readline.read.bind(readline);
|
|
xworker.sync.pyterminal_write = readline.write.bind(readline);
|
|
});
|
|
|
|
// setup remote thread JS/Python code for whenever the
|
|
// worker is ready to become a terminal
|
|
hooks.worker.onReady.add(workerReady);
|
|
} else {
|
|
// in the main case, just bootstrap XTerm without
|
|
// allowing any input as that's not possible / awkward
|
|
hooks.main.onReady.add(function main({ interpreter, io, run, type }) {
|
|
console.warn("py-terminal is read only on main thread");
|
|
hooks.main.onReady.delete(main);
|
|
|
|
// on main, it's easy to trash and clean the current terminal
|
|
globalThis.__py_terminal__ = init({
|
|
disableStdin: true,
|
|
cursorBlink: false,
|
|
cursorStyle: "underline",
|
|
});
|
|
run("from js import __py_terminal__ as __terminal__");
|
|
delete globalThis.__py_terminal__;
|
|
|
|
io.stderr = (error) => {
|
|
readline.write(`${error.message || error}\n`);
|
|
};
|
|
|
|
const isMicroPython = type === "mpy";
|
|
|
|
if (isMicroPython) {
|
|
interpreter.setStderr = Object; // as no-op
|
|
interpreter.setStdin = Object; // as no-op
|
|
interpreter.setStdout = ({ write }) => {
|
|
io.stdout = (str) => write(`${str}\n`);
|
|
};
|
|
}
|
|
|
|
// This part is inevitably duplicated as external scope
|
|
// can't be reached by workers out of the box.
|
|
// The detail is that here we use readline here, not sync.
|
|
const decoder = new TextDecoder();
|
|
let data = "";
|
|
const generic = {
|
|
isatty: true,
|
|
write(buffer) {
|
|
data = isMicroPython ? buffer : decoder.decode(buffer);
|
|
readline.write(data);
|
|
return buffer.length;
|
|
},
|
|
};
|
|
interpreter.setStdout(generic);
|
|
interpreter.setStderr(generic);
|
|
interpreter.setStdin({
|
|
isatty: true,
|
|
stdin: () => readline.read(data),
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
for (const key of TYPES.keys()) {
|
|
const selector = `script[type="${key}"][terminal],${key}-script[terminal]`;
|
|
SELECTORS.push(selector);
|
|
customObserver.set(selector, async (element) => {
|
|
// we currently support only one terminal on main as in "classic"
|
|
const terminals = document.querySelectorAll(SELECTORS.join(","));
|
|
if ([].filter.call(terminals, onceOnMain).length > 1)
|
|
notifyAndThrow("You can use at most 1 main terminal");
|
|
|
|
// import styles lazily
|
|
if (addStyle) {
|
|
addStyle = false;
|
|
document.head.append(
|
|
Object.assign(document.createElement("link"), {
|
|
rel: "stylesheet",
|
|
href: new URL("./xterm.css", import.meta.url),
|
|
}),
|
|
);
|
|
}
|
|
|
|
await pyTerminal(element);
|
|
});
|
|
}
|