From 6132695bd7d10c2779a34143e5a39065996771ba Mon Sep 17 00:00:00 2001 From: webreflection Date: Fri, 27 Oct 2023 18:33:47 +0200 Subject: [PATCH] PyScript Terminal - the latest kind --- pyscript.core/package-lock.json | 126 ++++++------- pyscript.core/package.json | 6 +- pyscript.core/src/config.js | 2 +- pyscript.core/src/core.js | 1 + pyscript.core/src/plugins/py-terminal.js | 158 +++++++++++++++++ pyscript.core/test/hooks.html | 2 + pyscript.core/test/mpy.html | 3 +- pyscript.core/test/mpy.spec.js | 4 +- pyscript.core/test/py-terminal.html | 28 +++ .../tests/integration/test_py_terminal.py | 167 ++---------------- pyscript.core/types/core.d.ts | 3 +- pyscript.core/types/plugins.d.ts | 7 +- pyscript.core/types/plugins/py-terminal.d.ts | 2 + 13 files changed, 282 insertions(+), 227 deletions(-) create mode 100644 pyscript.core/src/plugins/py-terminal.js create mode 100644 pyscript.core/test/py-terminal.html create mode 100644 pyscript.core/types/plugins/py-terminal.d.ts diff --git a/pyscript.core/package-lock.json b/pyscript.core/package-lock.json index 42e754ae768..993a0fa4549 100644 --- a/pyscript.core/package-lock.json +++ b/pyscript.core/package-lock.json @@ -1,17 +1,17 @@ { "name": "@pyscript/core", - "version": "0.3.0", + "version": "0.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pyscript/core", - "version": "0.3.0", + "version": "0.3.1", "license": "APACHE-2.0", "dependencies": { "@ungap/with-resolvers": "^0.1.0", "basic-devtools": "^0.1.6", - "polyscript": "^0.5.1", + "polyscript": "^0.5.6", "sticky-module": "^0.1.0", "to-json-callback": "^0.1.1", "type-checked-collections": "^0.1.7" @@ -23,7 +23,7 @@ "@webreflection/toml-j0.4": "^1.1.3", "chokidar": "^3.5.3", "eslint": "^8.52.0", - "rollup": "^4.1.4", + "rollup": "^4.2.0", "rollup-plugin-postcss": "^4.0.2", "rollup-plugin-string": "^3.0.0", "static-handler": "^0.4.3", @@ -306,9 +306,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.1.4.tgz", - "integrity": "sha512-WlzkuFvpKl6CLFdc3V6ESPt7gq5Vrimd2Yv9IzKXdOpgbH4cdDSS1JLiACX8toygihtH5OlxyQzhXOph7Ovlpw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.2.0.tgz", + "integrity": "sha512-8PlggAxGxavr+pkCNeV1TM2wTb2o+cUWDg9M1cm9nR27Dsn287uZtSLYXoQqQcmq+sYfF7lHfd3sWJJinH9GmA==", "cpu": [ "arm" ], @@ -319,9 +319,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.1.4.tgz", - "integrity": "sha512-D1e+ABe56T9Pq2fD+R3ybe1ylCDzu3tY4Qm2Mj24R9wXNCq35+JbFbOpc2yrroO2/tGhTobmEl2Bm5xfE/n8RA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.2.0.tgz", + "integrity": "sha512-+71T85hbMFrJI+zKQULNmSYBeIhru55PYoF/u75MyeN2FcxE4HSPw20319b+FcZ4lWx2Nx/Ql9tN+hoaD3GH/A==", "cpu": [ "arm64" ], @@ -332,9 +332,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.1.4.tgz", - "integrity": "sha512-7vTYrgEiOrjxnjsgdPB+4i7EMxbVp7XXtS+50GJYj695xYTTEMn3HZVEvgtwjOUkAP/Q4HDejm4fIAjLeAfhtg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.2.0.tgz", + "integrity": "sha512-IIIQLuG43QIElT1JZqUP/zqIdiJl4t9U/boa0GZnQTw9m1X0k3mlBuysbgYXeloLT1RozdL7bgw4lpSaI8GOXw==", "cpu": [ "arm64" ], @@ -345,9 +345,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.1.4.tgz", - "integrity": "sha512-eGJVZScKSLZkYjhTAESCtbyTBq9SXeW9+TX36ki5gVhDqJtnQ5k0f9F44jNK5RhAMgIj0Ht9+n6HAgH0gUUyWQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.2.0.tgz", + "integrity": "sha512-BXcXvnLaea1Xz900omrGJhxHFJfH9jZ0CpJuVsbjjhpniJ6qiLXz3xA8Lekaa4MuhFcJd4f0r+Ky1G4VFbYhWw==", "cpu": [ "x64" ], @@ -358,9 +358,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.1.4.tgz", - "integrity": "sha512-HnigYSEg2hOdX1meROecbk++z1nVJDpEofw9V2oWKqOWzTJlJf1UXVbDE6Hg30CapJxZu5ga4fdAQc/gODDkKg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.2.0.tgz", + "integrity": "sha512-f4K3MKw9Y4AKi4ANGnmPIglr+S+8tO858YrGVuqAHXxJdVghBmz9CPU9kDpOnGvT4g4vg5uNyIFpOOFvffXyMA==", "cpu": [ "arm" ], @@ -371,9 +371,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.1.4.tgz", - "integrity": "sha512-TzJ+N2EoTLWkaClV2CUhBlj6ljXofaYzF/R9HXqQ3JCMnCHQZmQnbnZllw7yTDp0OG5whP4gIPozR4QiX+00MQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.2.0.tgz", + "integrity": "sha512-bNsTYQBgp4H7w6cT7FZhesxpcUPahsSIy4NgdZjH1ZwEoZHxi4XKglj+CsSEkhsKi+x6toVvMylhjRKhEMYfnA==", "cpu": [ "arm64" ], @@ -384,9 +384,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.1.4.tgz", - "integrity": "sha512-aVPmNMdp6Dlo2tWkAduAD/5TL/NT5uor290YvjvFvCv0Q3L7tVdlD8MOGDL+oRSw5XKXKAsDzHhUOPUNPRHVTQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.2.0.tgz", + "integrity": "sha512-Jp1NxBJpGLuxRU2ihrQk4IZ+ia5nffobG6sOFUPW5PMYkF0kQtxEbeDuCa69Xif211vUOcxlOnf5IOEIpTEySA==", "cpu": [ "arm64" ], @@ -397,9 +397,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.1.4.tgz", - "integrity": "sha512-77Fb79ayiDad0grvVsz4/OB55wJRyw9Ao+GdOBA9XywtHpuq5iRbVyHToGxWquYWlEf6WHFQQnFEttsAzboyKg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.2.0.tgz", + "integrity": "sha512-3p3iRtQmv2aXw+vtKNyZMLOQ+LSRsqArXjKAh2Oj9cqwfIRe7OXvdkOzWfZOIp1F/x5KJzVAxGxnniF4cMbnsQ==", "cpu": [ "x64" ], @@ -410,9 +410,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.1.4.tgz", - "integrity": "sha512-/t6C6niEQTqmQTVTD9TDwUzxG91Mlk69/v0qodIPUnjjB3wR4UA3klg+orR2SU3Ux2Cgf2pWPL9utK80/1ek8g==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.2.0.tgz", + "integrity": "sha512-atih7IF/reUZe4LBLC5Izd44hth2tfDIG8LaPp4/cQXdHh9jabcZEvIeRPrpDq0i/Uu487Qu5gl5KwyAnWajnw==", "cpu": [ "x64" ], @@ -423,9 +423,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.1.4.tgz", - "integrity": "sha512-ZY5BHHrOPkMbCuGWFNpJH0t18D2LU6GMYKGaqaWTQ3CQOL57Fem4zE941/Ek5pIsVt70HyDXssVEFQXlITI5Gg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.2.0.tgz", + "integrity": "sha512-vYxF3tKJeUE4ceYzpNe2p84RXk/fGK30I8frpRfv/MyPStej/mRlojztkN7Jtd1014HHVeq/tYaMBz/3IxkxZw==", "cpu": [ "arm64" ], @@ -436,9 +436,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.1.4.tgz", - "integrity": "sha512-XG2mcRfFrJvYyYaQmvCIvgfkaGinfXrpkBuIbJrTl9SaIQ8HumheWTIwkNz2mktCKwZfXHQNpO7RgXLIGQ7HXA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.2.0.tgz", + "integrity": "sha512-1LZJ6zpl93SaPQvas618bMFarVwufWTaczH4ESAbFcwiC4OtznA6Ym+hFPyIGaJaGEB8uMWWac0uXGPXOg5FGA==", "cpu": [ "ia32" ], @@ -449,9 +449,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.1.4.tgz", - "integrity": "sha512-ANFqWYPwkhIqPmXw8vm0GpBEHiPpqcm99jiiAp71DbCSqLDhrtr019C5vhD0Bw4My+LmMvciZq6IsWHqQpl2ZQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.2.0.tgz", + "integrity": "sha512-dgQfFdHCNg08nM5zBmqxqc9vrm0DVzhWotpavbPa0j4//MAOKZEB75yGAfzQE9fUJ+4pvM1239Y4IhL8f6sSog==", "cpu": [ "x64" ], @@ -781,13 +781,13 @@ } }, "node_modules/coincident": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/coincident/-/coincident-0.14.2.tgz", - "integrity": "sha512-Xc/lh56dl/v5GT1R3bEWxiLzF5ZTiXE5Flcd0+qvrBGhZsvDha8bgqhpocrvJmELuerhDO3+EQKDdCzPBPodJQ==", + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/coincident/-/coincident-0.14.3.tgz", + "integrity": "sha512-vd5xP+d5vCCcwTTUxQb3LHRi+dhXnuD+Bgjyf1r1H0IPjfXGDs3z2C4RZJifCJmokqf3Ff9BiFealewTBMTgYw==", "dependencies": { "@ungap/structured-clone": "^1.2.0", "@ungap/with-resolvers": "^0.1.0", - "gc-hook": "^0.2.1" + "gc-hook": "^0.2.3" }, "optionalDependencies": { "ws": "^8.14.2" @@ -2121,15 +2121,15 @@ } }, "node_modules/polyscript": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/polyscript/-/polyscript-0.5.1.tgz", - "integrity": "sha512-jMFFWCJjYKcan7RV5UBVNg9IfVSss7wB6l5RIdx28pnBu1WSyA7sApLz7NEVbF14g5a5OjwtlvZCphQEA4CfQQ==", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/polyscript/-/polyscript-0.5.6.tgz", + "integrity": "sha512-T1iufSnsq33K5m2vECiVvgDd5zJiSum+eNv3/SUTb38vIxQpDG2W4aVffoIXIgPYe2Bij/aU2xW1P9M2CHUifw==", "dependencies": { "@ungap/structured-clone": "^1.2.0", "@ungap/with-resolvers": "^0.1.0", "basic-devtools": "^0.1.6", "codedent": "^0.1.2", - "coincident": "^0.14.2", + "coincident": "^0.14.3", "html-escaper": "^3.0.3", "sticky-module": "^0.1.0" } @@ -2814,9 +2814,9 @@ } }, "node_modules/rollup": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.1.4.tgz", - "integrity": "sha512-U8Yk1lQRKqCkDBip/pMYT+IKaN7b7UesK3fLSTuHBoBJacCE+oBqo/dfG/gkUdQNNB2OBmRP98cn2C2bkYZkyw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.2.0.tgz", + "integrity": "sha512-deaMa9Z+jPVeBD2dKXv+h7EbdKte9++V2potc/ADqvVgEr6DEJ3ia9u0joarjC2lX/ubaCRYz3QVx0TzuVqAJA==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -2826,18 +2826,18 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.1.4", - "@rollup/rollup-android-arm64": "4.1.4", - "@rollup/rollup-darwin-arm64": "4.1.4", - "@rollup/rollup-darwin-x64": "4.1.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.1.4", - "@rollup/rollup-linux-arm64-gnu": "4.1.4", - "@rollup/rollup-linux-arm64-musl": "4.1.4", - "@rollup/rollup-linux-x64-gnu": "4.1.4", - "@rollup/rollup-linux-x64-musl": "4.1.4", - "@rollup/rollup-win32-arm64-msvc": "4.1.4", - "@rollup/rollup-win32-ia32-msvc": "4.1.4", - "@rollup/rollup-win32-x64-msvc": "4.1.4", + "@rollup/rollup-android-arm-eabi": "4.2.0", + "@rollup/rollup-android-arm64": "4.2.0", + "@rollup/rollup-darwin-arm64": "4.2.0", + "@rollup/rollup-darwin-x64": "4.2.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.2.0", + "@rollup/rollup-linux-arm64-gnu": "4.2.0", + "@rollup/rollup-linux-arm64-musl": "4.2.0", + "@rollup/rollup-linux-x64-gnu": "4.2.0", + "@rollup/rollup-linux-x64-musl": "4.2.0", + "@rollup/rollup-win32-arm64-msvc": "4.2.0", + "@rollup/rollup-win32-ia32-msvc": "4.2.0", + "@rollup/rollup-win32-x64-msvc": "4.2.0", "fsevents": "~2.3.2" } }, diff --git a/pyscript.core/package.json b/pyscript.core/package.json index a84ecd0f854..3598b95eab2 100644 --- a/pyscript.core/package.json +++ b/pyscript.core/package.json @@ -1,6 +1,6 @@ { "name": "@pyscript/core", - "version": "0.3.0", + "version": "0.3.1", "type": "module", "description": "PyScript", "module": "./index.js", @@ -41,7 +41,7 @@ "dependencies": { "@ungap/with-resolvers": "^0.1.0", "basic-devtools": "^0.1.6", - "polyscript": "^0.5.1", + "polyscript": "^0.5.6", "sticky-module": "^0.1.0", "to-json-callback": "^0.1.1", "type-checked-collections": "^0.1.7" @@ -53,7 +53,7 @@ "@webreflection/toml-j0.4": "^1.1.3", "chokidar": "^3.5.3", "eslint": "^8.52.0", - "rollup": "^4.1.4", + "rollup": "^4.2.0", "rollup-plugin-postcss": "^4.0.2", "rollup-plugin-string": "^3.0.0", "static-handler": "^0.4.3", diff --git a/pyscript.core/src/config.js b/pyscript.core/src/config.js index 06aa53eece7..55b4c6cb391 100644 --- a/pyscript.core/src/config.js +++ b/pyscript.core/src/config.js @@ -113,7 +113,7 @@ for (const [TYPE] of TYPES) { value().then(({ notify }) => notify(error.message)); } } else if (!parsed?.plugins?.includes(`!${key}`)) { - toBeAwaited.push(value()); + toBeAwaited.push(value().then(({ default: p }) => p)); } } diff --git a/pyscript.core/src/core.js b/pyscript.core/src/core.js index 61567c0fb48..880ad2148dd 100644 --- a/pyscript.core/src/core.js +++ b/pyscript.core/src/core.js @@ -70,6 +70,7 @@ const [ }); export { + TYPES, exportedPyWorker as PyWorker, exportedHooks as hooks, exportedConfig as config, diff --git a/pyscript.core/src/plugins/py-terminal.js b/pyscript.core/src/plugins/py-terminal.js new file mode 100644 index 00000000000..49622ba414c --- /dev/null +++ b/pyscript.core/src/plugins/py-terminal.js @@ -0,0 +1,158 @@ +// PyScript py-terminal plugin +import { TYPES, hooks } from "../core.js"; + +const CDN = "https://cdn.jsdelivr.net/npm/xterm"; +const XTERM = "5.3.0"; +const XTERM_READLINE = "1.1.1"; +const SELECTOR = [...TYPES.keys()] + .map((type) => `script[type="${type}"][terminal],${type}-script[terminal]`) + .join(","); + +const pyTerminal = async () => { + const terminals = document.querySelectorAll(SELECTOR); + + // no results will look further for runtime nodes + if (!terminals.length) return; + + // we currently support only one terminal as in "classic" + if (terminals.length > 1) + console.warn("Unable to satisfy multiple terminals"); + + // if we arrived this far, let's drop the MutationObserver + mo.disconnect(); + + const [element] = terminals; + // hopefully to be removed in the near future! + if (element.matches('script[type="mpy"],mpy-script')) + throw new Error("Unsupported terminal"); + + // import styles once and lazily (only on valid terminal) + if (!document.querySelector(`link[href^="${CDN}"]`)) { + document.head.append( + Object.assign(document.createElement("link"), { + rel: "stylesheet", + href: `${CDN}@${XTERM}/css/xterm.min.css`, + }), + ); + } + + // lazy load these only when a valid terminal is found + const [{ Terminal }, { Readline }] = await Promise.all([ + import(/* webpackIgnore: true */ `${CDN}@${XTERM}/+esm`), + import( + /* webpackIgnore: true */ `${CDN}-readline@${XTERM_READLINE}/+esm` + ), + ]); + + 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(`${element.type}-terminal`); + target.style.display = "block"; + element.after(target); + } + const terminal = new Terminal({ + theme: { + background: "#191A19", + foreground: "#F5F2E7", + }, + ...options, + }); + terminal.loadAddon(readline); + terminal.open(target); + terminal.focus(); + }; + + // branch logic for the worker + if (element.hasAttribute("worker")) { + // when the remote thread onReady triggers: + // setup the interpreter stdout and stderr + const workerReady = ({ interpreter }, { sync }) => { + sync.pyterminal_drop_hooks(); + const decoder = new TextDecoder(); + const generic = { + isatty: true, + write(buffer) { + sync.pyterminal_write(decoder.decode(buffer)); + return buffer.length; + }, + }; + interpreter.setStdout(generic); + interpreter.setStderr(generic); + }; + + // run in python code able to replace builtins.input + // using the xworker.sync non blocking prompt + const codeBefore = ` + import builtins + from pyscript import sync as _sync + + builtins.input = lambda prompt: _sync.pyterminal_read(prompt) + `; + + // at the end of the code, make the terminal interactive + const codeAfter = ` + import code as _code + _code.interact() + `; + + // add a hook on the main thread to setup all sync helpers + // also bootstrapping the XTerm target on main + hooks.main.onWorker.add(function worker(_, xworker) { + hooks.main.onWorker.delete(worker); + init({ + disableStdin: false, + cursorBlink: true, + cursorStyle: "block", + }); + xworker.sync.pyterminal_read = readline.read.bind(readline); + xworker.sync.pyterminal_write = readline.write.bind(readline); + // allow a worker to drop main thread hooks ASAP + xworker.sync.pyterminal_drop_hooks = () => { + hooks.worker.onReady.delete(workerReady); + hooks.worker.codeBeforeRun.delete(codeBefore); + hooks.worker.codeAfterRun.delete(codeAfter); + }; + }); + + // setup remote thread JS/Python code for whenever the + // worker is ready to become a terminal + hooks.worker.onReady.add(workerReady); + hooks.worker.codeBeforeRun.add(codeBefore); + hooks.worker.codeAfterRun.add(codeAfter); + } else { + // in the main case, just bootstrap XTerm without + // allowing any input as that's not possible / awkward + hooks.main.onReady.add(function main({ io }) { + console.warn("py-terminal is read only on main thread"); + hooks.main.onReady.delete(main); + init({ + disableStdin: true, + cursorBlink: false, + cursorStyle: "underline", + }); + io.stdout = (value) => { + readline.write(`${value}\n`); + }; + io.stderr = (error) => { + readline.write(`${error.message || error}\n`); + }; + }); + } +}; + +const mo = new MutationObserver(pyTerminal); +mo.observe(document, { childList: true, subtree: true }); + +// try to check the current document ASAP +export default pyTerminal(); diff --git a/pyscript.core/test/hooks.html b/pyscript.core/test/hooks.html index c614dbd0ef9..b706a15dbae 100644 --- a/pyscript.core/test/hooks.html +++ b/pyscript.core/test/hooks.html @@ -49,7 +49,9 @@ + + + + + + import sys + from pyscript import display + display("Hello", "PyScript Next - PyTerminal", append=False) + print("this should go to the terminal") + print("another line") + + # this works as expected + print("this goes to stderr", file=sys.stderr) + + + + diff --git a/pyscript.core/tests/integration/test_py_terminal.py b/pyscript.core/tests/integration/test_py_terminal.py index 470e1fe63fd..9d9677c2678 100644 --- a/pyscript.core/tests/integration/test_py_terminal.py +++ b/pyscript.core/tests/integration/test_py_terminal.py @@ -1,17 +1,12 @@ import time -import pytest from playwright.sync_api import expect from .support import PyScriptTest, skip_worker -pytest.skip( - reason="FIX LATER: pyscript NEXT doesn't support the Terminal yet", - allow_module_level=True, -) - class TestPyTerminal(PyScriptTest): + @skip_worker("FIXME: the auto worker dance removes terminal") def test_py_terminal(self): """ 1. should redirect stdout and stderr to the DOM @@ -20,9 +15,7 @@ def test_py_terminal(self): """ self.pyscript_run( """ - - - - """ - ) - term1 = self.page.locator("#term1") - term2 = self.page.locator("#term2") - term1_lines = term1.inner_text().splitlines() - term2_lines = term2.inner_text().splitlines() - assert term1_lines == ["one", "two", "three"] - assert term2_lines == ["two", "three"] - - def test_auto_attribute(self): - self.pyscript_run( - """ - - - - """ - ) - term = self.page.locator("py-terminal") - expect(term).to_be_hidden() - self.page.locator("button").click() - expect(term).to_be_visible() - assert term.inner_text() == "hello world\n" - - def test_config_auto(self): - """ - config.terminal == "auto" is the default: a is - automatically added to the page - """ - self.pyscript_run( - """ - - """ - ) - term = self.page.locator("py-terminal") - expect(term).to_be_hidden() - assert "No found, adding one" in self.console.info.text - # - self.page.locator("button").click() - expect(term).to_be_visible() - assert term.inner_text() == "hello world\n" - - def test_config_true(self): - """ - If we set config.terminal == true, a is automatically added - """ + @skip_worker("FIXME: the auto worker dance removes terminal") + def test_button_action(self): self.pyscript_run( """ - - terminal = true - - - """ - ) - term = self.page.locator("py-terminal") - expect(term).to_be_visible() - assert term.inner_text() == "hello world\n" - - def test_config_false(self): - """ - If we set config.terminal == false, no is added - """ - self.pyscript_run( - """ - - terminal = false - - """ - ) - term = self.page.locator("py-terminal") - assert term.count() == 0 + - def test_config_docked(self): - """ - config.docked == "docked" is also the default: a is - automatically added to the page - """ - self.pyscript_run( - """ - + """ ) term = self.page.locator("py-terminal") self.page.locator("button").click() - expect(term).to_be_visible() - assert term.get_attribute("docked") == "" + last_line = self.page.get_by_text("hello world") + last_line.wait_for() + assert term.inner_text().rstrip() == "hello world" + @skip_worker("FIXME: the auto worker dance removes terminal") def test_xterm_function(self): """Test a few basic behaviors of the xtermjs terminal. @@ -164,10 +62,7 @@ def test_xterm_function(self): """ self.pyscript_run( """ - - xterm = true - - - - - """ - ) - - # Wait for "done" to actually appear in the xterm; may be delayed, - # since xtermjs processes its input buffer in chunks - last_line = self.page.get_by_test_id("b").get_by_text("done") - last_line.wait_for() - - # Yes, this is not ideal. See note in `test_xterm_function` - time.sleep(1) - - rows = self.page.locator("#a .xterm-rows") - - # First line should be yellow - first_line = rows.locator("div").nth(0) - first_char = first_line.locator("span").nth(0) - color = first_char.evaluate( - "(element) => getComputedStyle(element).getPropertyValue('color')" - ) - assert color == "rgb(196, 160, 0)" diff --git a/pyscript.core/types/core.d.ts b/pyscript.core/types/core.d.ts index 6ae562f2734..4a597f46742 100644 --- a/pyscript.core/types/core.d.ts +++ b/pyscript.core/types/core.d.ts @@ -1,5 +1,6 @@ +import TYPES from "./types.js"; declare const exportedPyWorker: any; declare const exportedHooks: any; declare const exportedConfig: any; declare const exportedWhenDefined: any; -export { exportedPyWorker as PyWorker, exportedHooks as hooks, exportedConfig as config, exportedWhenDefined as whenDefined }; +export { TYPES, exportedPyWorker as PyWorker, exportedHooks as hooks, exportedConfig as config, exportedWhenDefined as whenDefined }; 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..35a35db464a --- /dev/null +++ b/pyscript.core/types/plugins/py-terminal.d.ts @@ -0,0 +1,2 @@ +declare const _default: Promise; +export default _default;