From 185237d0a65de8bbb471a802e07951f0953d5245 Mon Sep 17 00:00:00 2001 From: Chris Laplante Date: Sun, 12 May 2024 23:24:28 -0400 Subject: [PATCH] clean up demo a bit --- src/components/JQueryTerminal.tsx | 6 +- src/components/PlaygroundTerminal.tsx | 191 +++++++++++++++++++++++--- src/hooks/useEnvironmentSetup.ts | 70 ++++++---- src/hooks/usePyodide.ts | 20 ++- 4 files changed, 237 insertions(+), 50 deletions(-) diff --git a/src/components/JQueryTerminal.tsx b/src/components/JQueryTerminal.tsx index 5a5ef28..bcec05e 100644 --- a/src/components/JQueryTerminal.tsx +++ b/src/components/JQueryTerminal.tsx @@ -3,6 +3,7 @@ import * as $ from "jquery"; import 'jquery.terminal'; import 'jquery.terminal/css/jquery.terminal.min.css'; import {terminal} from "jquery"; +import {usePyodide} from "../hooks/usePyodide"; interface Props { interpreter?: TypeOrArray, @@ -32,9 +33,8 @@ export const JQueryTerminal: React.ForwardRefExoticComponent { terminalObjectRef.current?.update(line, str); }, - freeze: () => { - terminalObjectRef.current?.freeze(true); - terminalObjectRef.current?.set_prompt(""); + freeze: (toggle?: boolean) => { + terminalObjectRef.current?.freeze(toggle); }, setPrompt: (prompt) => { terminalObjectRef.current?.set_prompt(prompt); diff --git a/src/components/PlaygroundTerminal.tsx b/src/components/PlaygroundTerminal.tsx index 7d26442..10f49a4 100644 --- a/src/components/PlaygroundTerminal.tsx +++ b/src/components/PlaygroundTerminal.tsx @@ -1,10 +1,10 @@ import {JQueryTerminal} from "./JQueryTerminal"; import React, {useEffect, useRef} from "react"; -import {usePyodide} from "../hooks/usePyodide"; - -import {terminal} from "jquery"; -import {useEnvironmentSetup} from "../hooks/useEnvironmentSetup"; +import {EnvironmentStatus, useEnvironmentSetup} from "../hooks/useEnvironmentSetup"; +import {PyodideStatus} from "../hooks/usePyodide"; +import {PyodideInterface} from "pyodide"; +// Taken from https://terminal.jcubic.pl/examples.php function progress(percent, width) { var size = Math.round(width*percent/100); var left = '', taken = '', i; @@ -20,31 +20,188 @@ function progress(percent, width) { return '[' + taken + left + '] ' + percent + '%'; } +// Terminal code largely taken from https://github.com/pyodide/pyodide/blob/main/src/templates/console.html +function sleep(s) { + return new Promise((resolve) => setTimeout(resolve, s)); +} + +function create_interpreter(pyodide: PyodideInterface, term: JQueryTerminal) { + let {repr_shorten, PyodideConsole} = pyodide.pyimport("pyodide.console"); + 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) => { + return term.echo( + msg + .replaceAll("]]", "]]") + .replaceAll("[[", "[["), + ...opts, + ); + }; + + async function lock() { + 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, term: JQueryTerminal) { + 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(); + } + + pyconsole.stdout_callback = (s) => echo(s, { newline: false }); + pyconsole.stderr_callback = (s) => { + term.error(s.trimEnd()); + }; + + return interpreter; +} + export const PlaygroundTerminal: React.FC = () => { const terminalRef = useRef(null); - const {state} = useEnvironmentSetup(); + const {state, pyodide} = useEnvironmentSetup(); + + const setupComplete = useRef(false); useEffect(() => { - terminalRef.current.echo("Setting up environment"); - terminalRef.current.freeze(); + terminalRef.current.echo("Setting up environment..."); + terminalRef.current.freeze(true); + terminalRef.current.setPrompt(""); }, []) useEffect(() => { - if (state.pyodideStatus !== "done unpacking") { + if (state.environmentStatus === EnvironmentStatus.WaitBitbakeOrPyodide) { + let s = ""; + switch (state.pyodideStatus) { + case PyodideStatus.Idle: + s = "idle"; + break; + case PyodideStatus.Fetching: + s = "fetching..."; + break; + case PyodideStatus.Loading: + s = "loading..."; + break; + case PyodideStatus.Done: + s = "done!"; + break; + case PyodideStatus.Inactive: + s = "inactive"; + break; + } + terminalRef.current.setPrompt( - `Downloading bitbake: ${progress(state.bitbakeProgress, 80)}%\nPyodide: ${state.pyodideStatus}` + `Downloading BitBake: ${progress(state.bitbakeProgress, 30)}%\nPyodide: ${s}` ) } else { - terminalRef.current.setPrompt( - `Done unpacking BitBake` - ) - } - }, [state]); + switch (state.environmentStatus) { + case EnvironmentStatus.UnpackingBitbake: + terminalRef.current.setPrompt( + `Unpacking BitBake...` + ) + break; + case EnvironmentStatus.LoadingSqlite3: + terminalRef.current.setPrompt( + `Loading sqlite3...` + ) + break; + case EnvironmentStatus.Configuring: + terminalRef.current.setPrompt( + `Installing import hooks...` + ) + break; + case EnvironmentStatus.ImportingBitbake: + terminalRef.current.setPrompt( + `Importing BitBake...` + ) + break; + case EnvironmentStatus.Ready: + if (!setupComplete.current) { + setupComplete.current = true; - const interpreter = (command, term) => { + terminalRef.current.setInterpreter(create_interpreter(pyodide, terminalRef.current)); - }; + terminalRef.current.echo("Ready :)\n"); + terminalRef.current.setPrompt(">>> "); + terminalRef.current.freeze(false); + } + break; + } + } + }, [pyodide, state]); - return () + return () } \ No newline at end of file diff --git a/src/hooks/useEnvironmentSetup.ts b/src/hooks/useEnvironmentSetup.ts index b70d5b3..89ec2bf 100644 --- a/src/hooks/useEnvironmentSetup.ts +++ b/src/hooks/useEnvironmentSetup.ts @@ -1,12 +1,26 @@ import {useImmerReducer} from "use-immer"; -import {usePyodide} from "./usePyodide"; -import {useEffect} from "react"; +import {PyodideStatus, usePyodide} from "./usePyodide"; +import {useEffect, useRef} from "react"; import {useSWRProgress} from "./useSWRProgress"; +export enum EnvironmentStatus { + WaitBitbakeOrPyodide, + LoadingSqlite3, + UnpackingBitbake, + Configuring, + ImportingBitbake, + Ready, +} +enum InternalStatus { + NotRun, + Running, + Done +} const initialEnvironmentState = { - pyodideStatus: "idle", + environmentStatus: EnvironmentStatus.WaitBitbakeOrPyodide, + pyodideStatus: PyodideStatus.Idle, bitbakeProgress: 0 }; @@ -18,6 +32,9 @@ function reducer(draft, action) { case "bitbakeProgressChanged": draft.bitbakeProgress = action.bitbakeProgress; return; + case "environmentStatusChanged": + draft.environmentStatus = action.environmentStatus; + break; } } @@ -33,19 +50,21 @@ export const useEnvironmentSetup = () => { useEffect(() => { dispatch({type: "bitbakeProgressChanged", bitbakeProgress: progress}); - }, [progress, dispatch]); + }, [dispatch, progress]); + + const effectStatus = useRef(InternalStatus.NotRun); useEffect(() => { - if (pyodide && data) { - dispatch({type: "pyodideStatusChanged", pyodideStatus: "Loading sqlite3"}); + if (pyodideStatus === PyodideStatus.Done && progress === 100 && effectStatus.current === InternalStatus.NotRun) { + effectStatus.current = InternalStatus.Running; const f = async() => { + dispatch({type: "environmentStatusChanged", environmentStatus: EnvironmentStatus.LoadingSqlite3}); await pyodide.loadPackage("sqlite3"); - dispatch({type: "pyodideStatusChanged", pyodideStatus: "done sqlite3"}); - dispatch({type: "pyodideStatusChanged", pyodideStatus: "Unpacking..."}); + dispatch({type: "environmentStatusChanged", pyodideStatus: EnvironmentStatus.UnpackingBitbake}); pyodide.unpackArchive(data, "zip", { extractDir: "bb" }); - dispatch({type: "pyodideStatusChanged", pyodideStatus: "done unpacking"}); + dispatch({type: "environmentStatusChanged", pyodideStatus: EnvironmentStatus.Configuring}); pyodide.runPython(` import os.path @@ -157,31 +176,34 @@ sys.meta_path.append(BuiltinImporterShim()) print(sys.meta_path) `) - const file = pyodide.FS.readdir("./bb"); - console.log(file); + // const file = pyodide.FS.readdir("./bb"); + // console.log(file); + dispatch({type: "environmentStatusChanged", environmentStatus: EnvironmentStatus.ImportingBitbake}); pyodide.runPython(` import sys sys.path.insert(0, "./bb/bitbake-2.8.0/lib/") from bb.data_smart import DataSmart `) - const DataSmart = pyodide.globals.get('DataSmart'); - const d = DataSmart(); - - d.setVar("A", "B"); - d.setVar("A:test", "C"); - d.setVar("OVERRIDES", "test"); - d.setVarFlag("A", "p", "OK"); - - console.log(d.getVar("A")); - - DataSmart.destroy(); + dispatch({type: "environmentStatusChanged", environmentStatus: EnvironmentStatus.Ready}); + // const DataSmart = pyodide.globals.get('DataSmart'); + // const d = DataSmart(); + // + // d.setVar("A", "B"); + // d.setVar("A:test", "C"); + // d.setVar("OVERRIDES", "test"); + // d.setVarFlag("A", "p", "OK"); + // + // console.log(d.getVar("A")); + // + // DataSmart.destroy(); } f(); + effectStatus.current = InternalStatus.Done; } - }, [data, dispatch, pyodide]); + }, [data, dispatch, progress, pyodide, pyodideStatus]); - return {state}; + return {state, pyodide}; }; \ No newline at end of file diff --git a/src/hooks/usePyodide.ts b/src/hooks/usePyodide.ts index 12f4d7a..65c94b9 100644 --- a/src/hooks/usePyodide.ts +++ b/src/hooks/usePyodide.ts @@ -4,20 +4,28 @@ import {PyodideInterface} from "pyodide"; let cachedInstance: PyodideInterface = null; -export const usePyodide: () => { pyodide: PyodideInterface; status: string } = () => { +export enum PyodideStatus { + Idle, + Fetching, + Loading, + Done, + Inactive, +} + +export const usePyodide: () => { pyodide: PyodideInterface; status: PyodideStatus } = () => { const [pyodide, setPyodide] = useState(null); - const [status, setStatus] = useState('idle'); + const [status, setStatus] = useState(PyodideStatus.Idle); useEffect(() => { let isActive = true; const loadPyodide = async () => { if (!cachedInstance) { - setStatus("importing"); + setStatus(PyodideStatus.Fetching); const { loadPyodide: loadPyodideModule } = await import("https://cdn.jsdelivr.net/pyodide/v0.25.1/full/pyodide.mjs"); - setStatus("loading"); + setStatus(PyodideStatus.Loading); cachedInstance = await loadPyodideModule(); - setStatus("done"); + setStatus(PyodideStatus.Done); } if (isActive) { setPyodide(cachedInstance); @@ -27,7 +35,7 @@ export const usePyodide: () => { pyodide: PyodideInterface; status: string } = ( loadPyodide(); return () => { - setStatus("inactive"); + setStatus(PyodideStatus.Inactive); isActive = false; }; }, []);