From fe8a06a6ff10e20cccb0dcf10f5698784d35d666 Mon Sep 17 00:00:00 2001 From: Chris Laplante Date: Sat, 11 May 2024 23:01:44 -0400 Subject: [PATCH] WIP --- package-lock.json | 259 +++++++++++++++++++++++++- package.json | 4 + src/components/App.tsx | 25 +-- src/components/FetchWithProgress.tsx | 2 +- src/components/JQueryTerminal.tsx | 68 +++++++ src/components/PlaygroundTerminal.tsx | 58 ++++++ src/components/TerminalComponent.tsx | 171 +++++++++++++++++ src/hooks/useEnvironmentSetup.ts | 37 ++++ src/{ => hooks}/usePyodide.ts | 14 +- src/hooks/useSWRProgress.ts | 21 +++ 10 files changed, 628 insertions(+), 31 deletions(-) create mode 100644 src/components/JQueryTerminal.tsx create mode 100644 src/components/PlaygroundTerminal.tsx create mode 100644 src/components/TerminalComponent.tsx create mode 100644 src/hooks/useEnvironmentSetup.ts rename src/{ => hooks}/usePyodide.ts (64%) create mode 100644 src/hooks/useSWRProgress.ts diff --git a/package-lock.json b/package-lock.json index acb4e57..8d9fbde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,11 +13,14 @@ "axios": "^1.6.8", "classnames": "^2.5.1", "immer": "^10.1.1", + "jquery": "^3.7.1", + "jquery.terminal": "^2.41.2", "pyodide": "^0.25.1", "react": "^18.2.0", "react-ace": "^11.0.1", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.13", + "swr": "^2.2.5", "use-immer": "^0.9.0" }, "devDependencies": { @@ -26,6 +29,7 @@ "@babel/preset-react": "^7.24.1", "@babel/preset-typescript": "^7.24.1", "@mantine/core": "^7.9.1", + "@types/jquery": "^3.5.30", "@types/node": "^20.12.11", "@types/react": "^18.3.2", "@types/react-dom": "^18.3.0", @@ -2152,6 +2156,11 @@ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, + "node_modules/@jcubic/lily": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@jcubic/lily/-/lily-0.3.0.tgz", + "integrity": "sha512-4z6p4jLGSthc8gQ7wu4nHfGYn/IgCKFr+7hjuf80VdXUs7sm029mZGGDpS8sb29PVZWUBvMMTBCVGFhH2nN4Vw==" + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -2429,6 +2438,14 @@ "@types/node": "*" } }, + "node_modules/@types/jquery": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.30.tgz", + "integrity": "sha512-nbWKkkyb919DOUxjmRVk8vwtDb0/k8FKncmUKFi+NY+QXqWltooxTrswvz4LspQwxvLdvzBN1TImr6cw3aQx2A==", + "dependencies": { + "@types/sizzle": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2538,6 +2555,11 @@ "@types/send": "*" } }, + "node_modules/@types/sizzle": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", + "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==" + }, "node_modules/@types/sockjs": { "version": "0.3.36", "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", @@ -3177,6 +3199,11 @@ "node": ">=4" } }, + "node_modules/ansidec": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/ansidec/-/ansidec-0.3.4.tgz", + "integrity": "sha512-Ydgbey4zqUmmNN2i2OVeVHXig3PxHRbok2X6B2Sogmb92JzZUFfTL806dT7os6tBL1peXItfeFt76CP3zsoXUg==" + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -3199,8 +3226,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/array-buffer-byte-length": { "version": "1.0.1", @@ -3794,6 +3820,19 @@ "node": ">= 10.0" } }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -4085,6 +4124,24 @@ } } }, + "node_modules/coveralls-next": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/coveralls-next/-/coveralls-next-4.2.1.tgz", + "integrity": "sha512-O/SBGZsCryt+6Q3NuJHENyQYaucTEV9qp0KGaed+y42PUh+GuF949LRLHKZbxWwOIc1tV8bJRIVWlfbZ8etEwQ==", + "dependencies": { + "form-data": "4.0.0", + "js-yaml": "4.1.0", + "lcov-parse": "1.0.0", + "log-driver": "1.2.7", + "minimist": "1.2.8" + }, + "bin": { + "coveralls": "bin/coveralls.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -4198,6 +4255,14 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "dev": true }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -4299,6 +4364,17 @@ "node": ">= 10" } }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -5347,6 +5423,28 @@ "node": ">=0.8.0" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -5544,6 +5642,17 @@ "node": ">= 6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -5591,7 +5700,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -6881,6 +6989,44 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" + }, + "node_modules/jquery.terminal": { + "version": "2.41.2", + "resolved": "https://registry.npmjs.org/jquery.terminal/-/jquery.terminal-2.41.2.tgz", + "integrity": "sha512-wadUWw1pOwRb2ZVIMLPT+D0Qnsql7yrUVsLqc6/l7w00biRanZHoj3rdMu0SZCbg+wzGSdBQXQ4r7viR32+W8w==", + "dependencies": { + "@jcubic/lily": "^0.3.0", + "@types/jquery": "^3.5.29", + "ansidec": "^0.3.4", + "coveralls-next": "^4.2.1", + "iconv-lite": "^0.6.3", + "jquery": "^3.7.1", + "node-fetch": "^3.3.2", + "prismjs": "^1.27.0", + "wcwidth": "^1.0.1" + }, + "bin": { + "from-ansi": "bin/convert.js" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jquery.terminal/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6890,7 +7036,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -6995,6 +7140,14 @@ "shell-quote": "^1.8.1" } }, + "node_modules/lcov-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", + "integrity": "sha512-aprLII/vPzuQvYZnDRU78Fns9I2Ag3gi4Ipga/hxnVMCZC8DnR2nI7XBqrPoywGfxqIx/DgarGvDJZAD3YBTgQ==", + "bin": { + "lcov-parse": "bin/cli.js" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -7125,6 +7278,14 @@ "integrity": "sha512-qrRMbykBSEGdOgQLJJqVSdPWMD7Q+GJJ5jMRfQYb+LTLsw3tYVIabnCzRqTJb2WTo17PG5gNzXuFaZgYH/9SAQ==", "dev": true }, + "node_modules/log-driver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", + "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", + "engines": { + "node": ">=0.8.6" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -7305,6 +7466,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/mrmime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", @@ -7382,6 +7551,41 @@ "tslib": "^2.0.3" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -8174,6 +8378,14 @@ "renderkid": "^3.0.0" } }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -8816,8 +9028,7 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass": { "version": "1.77.1", @@ -9487,6 +9698,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swr": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.5.tgz", + "integrity": "sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==", + "dependencies": { + "client-only": "^0.0.1", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/tabbable": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", @@ -10137,6 +10360,14 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -10204,6 +10435,22 @@ "minimalistic-assert": "^1.0.0" } }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, "node_modules/webpack": { "version": "5.91.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz", diff --git a/package.json b/package.json index 593ae2c..4d10e4e 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@babel/preset-react": "^7.24.1", "@babel/preset-typescript": "^7.24.1", "@mantine/core": "^7.9.1", + "@types/jquery": "^3.5.30", "@types/node": "^20.12.11", "@types/react": "^18.3.2", "@types/react-dom": "^18.3.0", @@ -67,11 +68,14 @@ "axios": "^1.6.8", "classnames": "^2.5.1", "immer": "^10.1.1", + "jquery": "^3.7.1", + "jquery.terminal": "^2.41.2", "pyodide": "^0.25.1", "react": "^18.2.0", "react-ace": "^11.0.1", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.13", + "swr": "^2.2.5", "use-immer": "^0.9.0" } } diff --git a/src/components/App.tsx b/src/components/App.tsx index 4b26253..5fdfcd0 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -3,11 +3,10 @@ import React, {useEffect, useState} from "react"; import {AppShell, Burger, createTheme, MantineColorsTuple, MantineProvider} from '@mantine/core'; import FetchWithProgress from "./FetchWithProgress"; -import AceEditor from "react-ace"; -import "ace-builds/src-noconflict/mode-jsx"; -import {usePyodide} from "../usePyodide"; +import {usePyodide} from "../hooks/usePyodide"; import {useDisclosure} from "@mantine/hooks"; import {useImmer} from "use-immer"; +import {PlaygroundTerminal} from "./PlaygroundTerminal"; const myColor: MantineColorsTuple = [ '#e4f8ff', @@ -40,13 +39,9 @@ const Inner: React.FC = () => { useEffect(() => { const go = async () => { if (data && !ran) { - setRan(true); - - pyodide.setStdout({ - batched: (msg) => setOutput(o => { o.push(msg); }) - }) console.warn("LOADING SQLITE"); + await pyodide.loadPackage("sqlite3"); console.warn("LOADED!"); @@ -183,6 +178,7 @@ print(sys.meta_path) console.log(d.getVar("A")); DataSmart.destroy(); + setRan(true); } else { console.warn(`data = ${!!data}, p = ${!!pyodide}`); @@ -192,8 +188,9 @@ print(sys.meta_path) go(); }, [data, pyodide, ran, setRan]); - return <> + {/*{pyodide && data && ran && }*/} +

pyodide: {pyodideStatus}

    @@ -207,7 +204,6 @@ export const App: React.FC = () => { return ( - { Navbar + - - - diff --git a/src/components/FetchWithProgress.tsx b/src/components/FetchWithProgress.tsx index 5fd5bdd..f298a85 100644 --- a/src/components/FetchWithProgress.tsx +++ b/src/components/FetchWithProgress.tsx @@ -18,7 +18,7 @@ const FetchWithProgress: React.FC = (props: FetchProgressPro setLoading(true); const ret = await axios.get(props.url, { onDownloadProgress: progressEvent => { - const percentage = Math.round(progressEvent.loaded * 100) / progressEvent.total; + const percentage = Math.round((progressEvent.loaded * 100) / progressEvent.total); console.log(percentage); setProgress(percentage); }, diff --git a/src/components/JQueryTerminal.tsx b/src/components/JQueryTerminal.tsx new file mode 100644 index 0000000..20eb40e --- /dev/null +++ b/src/components/JQueryTerminal.tsx @@ -0,0 +1,68 @@ +import React, {useImperativeHandle, forwardRef, useRef, useEffect} from "react"; +import * as $ from "jquery"; +import 'jquery.terminal'; +import 'jquery.terminal/css/jquery.terminal.min.css'; +import {terminal} from "jquery"; + +interface Props { + interpreter?: TypeOrArray, + options?: JQueryTerminal.TerminalOptions +} + +const BANNER = ` + __ __ __ __ _____ __ __ _____ __ ___ ___ ___ _ __ __ __ __ ___ __ _ _ __ _ __ +| \\ \\ | _\\ / \\_ _/ \\ /' _/_ _/__\\| _ \\ __| | _,\\ | / \\\\ \`v' // _] _ \\/__\\| || | \\| | _\\ +| -< -< | v | /\\ || || /\\ |\`._\`. | || \\/ | v / _| | v_/ |_| /\\ |\`. .'| [/\\ v / \\/ | \\/ | | ' | v | +|__/__/ |__/|_||_||_||_||_||___/ |_| \\__/|_|_\\___| |_| |___|_||_| !_! \\__/_|_\\\\__/ \\__/|_|\\__|__/ + +Copyright (C) Agilent Technologies 2024 +`; + +export const JQueryTerminal: React.ForwardRefExoticComponent & React.RefAttributes> = forwardRef(function JQueryTerminal(props, ref) { + const terminalContainerRef = useRef(null); + const terminalObjectRef = useRef(null); + + useImperativeHandle(ref, () => { + return { + echo: async (arg: string, options: JQueryTerminal.animationOptions & JQueryTerminal.EchoOptions) => { + if (terminalObjectRef.current) { + return terminalObjectRef.current.echo(arg, options); + } + }, + update: (line: number, str: string) => { + terminalObjectRef.current?.update(line, str); + }, + freeze: () => { + terminalObjectRef.current?.freeze(true); + terminalObjectRef.current?.set_prompt(""); + }, + setInterpreter: (interpreter?: TypeOrArray) => { + if (terminalObjectRef.current) { + terminalObjectRef.current.set_interpreter(interpreter); + } + } + }; + }, []); + + useEffect(() => { + const currentTerminal = terminalContainerRef.current; + + if (currentTerminal) { + terminalObjectRef.current = $(currentTerminal).terminal(props.interpreter, { + greetings: BANNER, + ...props.options + }); + } + + return () => { + if (currentTerminal) { + $(currentTerminal).remove(); + } + if (terminalObjectRef.current) { + terminalObjectRef.current = null; + } + }; + }, []); + + return
    ; +}); \ No newline at end of file diff --git a/src/components/PlaygroundTerminal.tsx b/src/components/PlaygroundTerminal.tsx new file mode 100644 index 0000000..7787448 --- /dev/null +++ b/src/components/PlaygroundTerminal.tsx @@ -0,0 +1,58 @@ +import {JQueryTerminal} from "./JQueryTerminal"; +import React, {useEffect, useRef} from "react"; +import {usePyodide} from "../hooks/usePyodide"; + +import {terminal} from "jquery"; +import {useEnvironmentSetup} from "../hooks/useEnvironmentSetup"; + +function progress(percent, width) { + var size = Math.round(width*percent/100); + var left = '', taken = '', i; + for (i=size; i--;) { + taken += '='; + } + if (taken.length > 0) { + taken = taken.replace(/=$/, '>'); + } + for (i=width-size; i--;) { + left += ' '; + } + return '[' + taken + left + '] ' + percent + '%'; +} + +export const PlaygroundTerminal: React.FC = () => { + const terminalRef = useRef(null); + + const {state} = useEnvironmentSetup(); + + useEffect(() => { + terminalRef.current.echo("Setting up environment"); + terminalRef.current.freeze(); + }, []) + + useEffect(() => { + if (state.pyodideStatus === "idle") { + terminalRef.current.echo(`Pyodide: ${state.pyodideStatus}`); + terminalRef.current.echo(`Downloading bitbake: ${progress(state.bitbakeProgress, 80)}%`); + } + + terminalRef.current.update(-1, `Pyodide: ${state.pyodideStatus}`); + terminalRef.current.update(-2, `Downloading bitbake: ${progress(state.bitbakeProgress, 80)}%`); + }, [state]); + // + // useEffect(() => { + // if (done && pyodide) { + // terminalRef.current.echo("Unpacking BitBake..."); + // pyodide.unpackArchive(data, "zip", { + // extractDir: "bb" + // }); + // terminalRef.current.echo("Done!"); + // } + // }, [data, done, pyodide]); + + const interpreter = (command, term) => { + + }; + + return () +} \ No newline at end of file diff --git a/src/components/TerminalComponent.tsx b/src/components/TerminalComponent.tsx new file mode 100644 index 0000000..72ce1c6 --- /dev/null +++ b/src/components/TerminalComponent.tsx @@ -0,0 +1,171 @@ +import React, {useEffect, useRef} from 'react'; +import * as $ from "jquery"; +import 'jquery.terminal'; +import 'jquery.terminal/css/jquery.terminal.min.css'; +import {PyodideInterface} from "pyodide"; + + +interface Props { + pyodide: PyodideInterface +} + +const TerminalComponent: React.FC = (props) => { + const terminalContainerRef = useRef(null); + const terminalObjectRef = useRef(null); + + function sleep(s) { + return new Promise((resolve) => setTimeout(resolve, s)); + } + + const pyodide = props.pyodide; + let {repr_shorten, BANNER, PyodideConsole} = pyodide.pyimport("pyodide.console"); + + pyodide.setStdin({ + stdin: () => { + const result = prompt(); + echo(result); + return result; + } + }) + + 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) => { + if (terminalObjectRef.current) { + return terminalObjectRef.current.echo( + msg + .replaceAll("]]", "]]") + .replaceAll("[[", "[["), + ...opts, + ); + } + }; + + async function lock() { + if (!terminalObjectRef.current) { + return; + } + + const term = terminalObjectRef.current; + + let resolve; + const ready = term.ready; + term.ready = new Promise((res) => (resolve = res)); + await ready; + return resolve; + } + + const ps1 = ">>> "; + const ps2 = "... "; + + async function interpreter(command) { + if (!terminalObjectRef.current) { + return; + } + + const term = terminalObjectRef.current; + + 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\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(); + } + + useEffect(() => { + const currentTerminal = terminalContainerRef.current; + + if (currentTerminal) { + terminalObjectRef.current = $(currentTerminal).terminal(interpreter, { + prompt: ">>>", + }); + + pyconsole.stdout_callback = (s) => echo(s, { newline: false }); + pyconsole.stderr_callback = (s) => { + terminalObjectRef.current.error(s.trimEnd()); + }; + } + + // Cleanup function to be called when the component unmounts + return () => { + if (currentTerminal) { + $(currentTerminal).remove(); + } + if (terminalObjectRef.current) { + terminalObjectRef.current = null; + } + }; + }, [interpreter, pyconsole]); // Empty dependency array ensures this effect runs only once after initial render + + // Render a div that will host the jQuery Terminal + return
    ; +}; + +export default TerminalComponent; diff --git a/src/hooks/useEnvironmentSetup.ts b/src/hooks/useEnvironmentSetup.ts new file mode 100644 index 0000000..15ac62d --- /dev/null +++ b/src/hooks/useEnvironmentSetup.ts @@ -0,0 +1,37 @@ +import {useImmerReducer} from "use-immer"; +import {usePyodide} from "./usePyodide"; +import {useEffect} from "react"; +import {useSWRProgress} from "./useSWRProgress"; + +const initialEnvironmentState = { + pyodideStatus: "idle", + bitbakeProgress: 0 +}; + +function reducer(draft, action) { + switch (action.type) { + case "pyodideStatusChanged": + draft.pyodideStatus = action.pyodideStatus; + return; + case "bitbakeProgressChanged": + draft.bitbakeProgress = action.bitbakeProgress; + return; + } +} + +export const useEnvironmentSetup = () => { + const [state, dispatch] = useImmerReducer(reducer, initialEnvironmentState); + + const [{data}, {progress, done}] = useSWRProgress("assets/bitbake-2.8.0.zip"); + const {pyodide, status: pyodideStatus } = usePyodide(); + + useEffect(() => { + dispatch({type: "pyodideStatusChanged", pyodideStatus: pyodideStatus}); + }, [dispatch, pyodideStatus]); + + useEffect(() => { + dispatch({type: "bitbakeProgressChanged", bitbakeProgress: progress}); + }, [progress, dispatch]); + + return {state}; +}; \ No newline at end of file diff --git a/src/usePyodide.ts b/src/hooks/usePyodide.ts similarity index 64% rename from src/usePyodide.ts rename to src/hooks/usePyodide.ts index 78d1144..12f4d7a 100644 --- a/src/usePyodide.ts +++ b/src/hooks/usePyodide.ts @@ -1,24 +1,26 @@ import {useEffect, useState} from 'react'; -import pyodide from "pyodide"; +import {PyodideInterface} from "pyodide"; -export const usePyodide = () => { - const [pyodide, setPyodide] = useState(null); +let cachedInstance: PyodideInterface = null; + +export const usePyodide: () => { pyodide: PyodideInterface; status: string } = () => { + const [pyodide, setPyodide] = useState(null); const [status, setStatus] = useState('idle'); useEffect(() => { let isActive = true; const loadPyodide = async () => { - if (!window.pyodide) { + if (!cachedInstance) { setStatus("importing"); const { loadPyodide: loadPyodideModule } = await import("https://cdn.jsdelivr.net/pyodide/v0.25.1/full/pyodide.mjs"); setStatus("loading"); - window.pyodide = await loadPyodideModule(); + cachedInstance = await loadPyodideModule(); setStatus("done"); } if (isActive) { - setPyodide(window.pyodide); + setPyodide(cachedInstance); } }; diff --git a/src/hooks/useSWRProgress.ts b/src/hooks/useSWRProgress.ts new file mode 100644 index 0000000..f1c54d8 --- /dev/null +++ b/src/hooks/useSWRProgress.ts @@ -0,0 +1,21 @@ +import {useRef, useState} from "react"; +import axios from "axios"; +import useSWR from "swr"; + +export const useSWRProgress = (key, options?) => { + const [progress, setProgress] = useState(0); + const [done, setDone] = useState(false); + + return [useSWR(key, (url) => axios.get(url, { + onDownloadProgress: progressEvent => { + const percentage = Math.round((progressEvent.loaded * 100) / progressEvent.total); + console.log(percentage); + setProgress(percentage); + }, + responseType: "arraybuffer" + }).then((res) => { + setDone(true); + return res.data; + }) + ), {progress, done}] +} \ No newline at end of file