diff --git a/pyscript.core/src/plugins/py-terminal.js b/pyscript.core/src/plugins/py-terminal.js new file mode 100644 index 00000000000..cfc9da5de09 --- /dev/null +++ b/pyscript.core/src/plugins/py-terminal.js @@ -0,0 +1,114 @@ +// PyScript py-terminal plugin +import { hooks } from "../core.js"; + +const XTERM = "5.3.0"; +const XTERM_READLINE = "1.1.1"; + +const { assign } = Object; + +// Avoid conflicts with py-terminal re-definition +if (!customElements.get("py-terminal")) { + document.head.append( + assign(document.createElement("link"), { + rel: "stylesheet", + href: `https://cdn.jsdelivr.net/npm/xterm@${XTERM}/css/xterm.min.css`, + }), + ); + + const [{ Terminal }, { Readline }] = await Promise.all([ + import( + /* webpackIgnore: true */ `https://cdn.jsdelivr.net/npm/xterm@${XTERM}/+esm` + ), + import( + /* webpackIgnore: true */ `https://cdn.jsdelivr.net/npm/xterm-readline@${XTERM_READLINE}/+esm` + ), + ]); + + customElements.define( + "py-terminal", + class extends HTMLElement { + #terminal; + #readline; + init(options) { + this.#readline = new Readline(); + this.#terminal = new Terminal({ + theme: { + background: "#191A19", + foreground: "#F5F2E7", + }, + ...options, + }); + this.#terminal.loadAddon(this.#readline); + this.#terminal.open(this); + this.#terminal.focus(); + } + readline(prompt) { + return this.#readline.read(prompt); + } + write(line) { + this.#readline.write(line); + } + }, + ); + + const codeBefore = ` + from pyscript import pyterminal as _pyterminal + _pyterminal.init() + `; + + const codeAfter = ` + from pyscript import pyterminal as _pyterminal + + # avoid bootstrapping interact() if no terminal exists + if _pyterminal.PY_TERMINAL: + import code as _code + _code.interact() + `; + + const main = ({ io }) => { + const pt = document.querySelector("py-terminal"); + if (pt) { + cleanUp(true); + pt.init({ + disableStdin: true, + cursorBlink: false, + cursorStyle: "underline", + }); + io.stdout = (value) => { + pt.write(`${value}\n`); + }; + io.stderr = (error) => { + pt.write(`${error.message || error}\n`); + }; + } + }; + + const thread = (_, xworker) => { + const pt = document.querySelector("py-terminal"); + if (pt) { + cleanUp(false); + pt.init({ + disableStdin: false, + cursorBlink: true, + cursorStyle: "block", + }); + xworker.sync.pyterminal_readline = pt.readline.bind(pt); + xworker.sync.pyterminal_write = pt.write.bind(pt); + } + }; + + // we currently support only one + const cleanUp = (fromMain) => { + hooks.onInterpreterReady.delete(main); + hooks.onWorkerReady.delete(thread); + if (fromMain) { + hooks.codeBeforeRunWorker.delete(codeBefore); + hooks.codeAfterRunWorker.delete(codeBefore); + } + }; + + hooks.onInterpreterReady.add(main); + hooks.onWorkerReady.add(thread); + hooks.codeBeforeRunWorker.add(codeBefore); + hooks.codeAfterRunWorker.add(codeAfter); +} diff --git a/pyscript.core/src/stdlib/pyscript/pyterminal.py b/pyscript.core/src/stdlib/pyscript/pyterminal.py new file mode 100644 index 00000000000..6d1e06d39a5 --- /dev/null +++ b/pyscript.core/src/stdlib/pyscript/pyterminal.py @@ -0,0 +1,26 @@ +import builtins +import sys + +import js +from pyscript import RUNNING_IN_WORKER, document, sync + + +class _PyTerminal: + def write(self, line): + sync.pyterminal_write(line) + + def input(self, prompt): + return sync.pyterminal_readline(prompt) + + +PY_TERMINAL = None + + +def init(): + global PY_TERMINAL + # we currently support only one terminal + # and it's interactive only within a worker tag + if not PY_TERMINAL and RUNNING_IN_WORKER and document.querySelector("py-terminal"): + PY_TERMINAL = _PyTerminal() + sys.stdout = sys.stderr = PY_TERMINAL + builtins.input = PY_TERMINAL.input diff --git a/pyscript.core/test/py-terminal.html b/pyscript.core/test/py-terminal.html new file mode 100644 index 00000000000..c0949edfbba --- /dev/null +++ b/pyscript.core/test/py-terminal.html @@ -0,0 +1,24 @@ + + + + + + PyTerminal + + + + + + + + + diff --git a/pyscript.core/types/plugins.d.ts b/pyscript.core/types/plugins.d.ts index 1afe130852c..abce670d72d 100644 --- a/pyscript.core/types/plugins.d.ts +++ b/pyscript.core/types/plugins.d.ts @@ -1,4 +1,5 @@ -declare namespace _default { - function error(): Promise; -} +declare const _default: { + error: () => Promise; + "py-terminal": () => Promise; +}; export default _default; diff --git a/pyscript.core/types/plugins/py-terminal.d.ts b/pyscript.core/types/plugins/py-terminal.d.ts new file mode 100644 index 00000000000..cb0ff5c3b54 --- /dev/null +++ b/pyscript.core/types/plugins/py-terminal.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/pyscript.core/types/stdlib/pyscript.d.ts b/pyscript.core/types/stdlib/pyscript.d.ts index fa33defe76c..12891e9ca3d 100644 --- a/pyscript.core/types/stdlib/pyscript.d.ts +++ b/pyscript.core/types/stdlib/pyscript.d.ts @@ -4,6 +4,7 @@ declare namespace _default { "display.py": string; "event_handling.py": string; "magic_js.py": string; + "pyterminal.py": string; "util.py": string; }; let pyweb: {