Skip to content

Commit 185237d

Browse files
committed
clean up demo a bit
1 parent 23f7fcc commit 185237d

File tree

4 files changed

+237
-50
lines changed

4 files changed

+237
-50
lines changed

src/components/JQueryTerminal.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as $ from "jquery";
33
import 'jquery.terminal';
44
import 'jquery.terminal/css/jquery.terminal.min.css';
55
import {terminal} from "jquery";
6+
import {usePyodide} from "../hooks/usePyodide";
67

78
interface Props {
89
interpreter?: TypeOrArray<JQueryTerminal.Interpreter>,
@@ -32,9 +33,8 @@ export const JQueryTerminal: React.ForwardRefExoticComponent<React.PropsWithoutR
3233
update: (line: number, str: string) => {
3334
terminalObjectRef.current?.update(line, str);
3435
},
35-
freeze: () => {
36-
terminalObjectRef.current?.freeze(true);
37-
terminalObjectRef.current?.set_prompt("");
36+
freeze: (toggle?: boolean) => {
37+
terminalObjectRef.current?.freeze(toggle);
3838
},
3939
setPrompt: (prompt) => {
4040
terminalObjectRef.current?.set_prompt(prompt);

src/components/PlaygroundTerminal.tsx

Lines changed: 174 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import {JQueryTerminal} from "./JQueryTerminal";
22
import React, {useEffect, useRef} from "react";
3-
import {usePyodide} from "../hooks/usePyodide";
4-
5-
import {terminal} from "jquery";
6-
import {useEnvironmentSetup} from "../hooks/useEnvironmentSetup";
3+
import {EnvironmentStatus, useEnvironmentSetup} from "../hooks/useEnvironmentSetup";
4+
import {PyodideStatus} from "../hooks/usePyodide";
5+
import {PyodideInterface} from "pyodide";
76

7+
// Taken from https://terminal.jcubic.pl/examples.php
88
function progress(percent, width) {
99
var size = Math.round(width*percent/100);
1010
var left = '', taken = '', i;
@@ -20,31 +20,188 @@ function progress(percent, width) {
2020
return '[' + taken + left + '] ' + percent + '%';
2121
}
2222

23+
// Terminal code largely taken from https://github.com/pyodide/pyodide/blob/main/src/templates/console.html
24+
function sleep(s) {
25+
return new Promise((resolve) => setTimeout(resolve, s));
26+
}
27+
28+
function create_interpreter(pyodide: PyodideInterface, term: JQueryTerminal) {
29+
let {repr_shorten, PyodideConsole} = pyodide.pyimport("pyodide.console");
30+
const pyconsole = PyodideConsole(pyodide.globals);
31+
32+
const namespace = pyodide.globals.get("dict")();
33+
const await_fut = pyodide.runPython(
34+
`
35+
import builtins
36+
from pyodide.ffi import to_js
37+
38+
async def await_fut(fut):
39+
res = await fut
40+
if res is not None:
41+
builtins._ = res
42+
return to_js([res], depth=1)
43+
44+
await_fut
45+
`,
46+
{globals: namespace},
47+
);
48+
namespace.destroy();
49+
50+
const echo = (msg, ...opts) => {
51+
return term.echo(
52+
msg
53+
.replaceAll("]]", "&rsqb;&rsqb;")
54+
.replaceAll("[[", "&lsqb;&lsqb;"),
55+
...opts,
56+
);
57+
};
58+
59+
async function lock() {
60+
let resolve;
61+
const ready = term.ready;
62+
term.ready = new Promise((res) => (resolve = res));
63+
await ready;
64+
return resolve;
65+
}
66+
67+
const ps1 = ">>> ";
68+
const ps2 = "... ";
69+
70+
async function interpreter(command, term: JQueryTerminal) {
71+
const unlock = await lock();
72+
term.pause();
73+
// multiline should be split (useful when pasting)
74+
for (const c of command.split("\n")) {
75+
const escaped = c.replaceAll(/\u00a0/g, " ");
76+
const fut = pyconsole.push(escaped);
77+
term.set_prompt(fut.syntax_check === "incomplete" ? ps2 : ps1);
78+
switch (fut.syntax_check) {
79+
case "syntax-error":
80+
term.error(fut.formatted_error.trimEnd());
81+
continue;
82+
case "incomplete":
83+
continue;
84+
case "complete":
85+
break;
86+
default:
87+
throw new Error(`Unexpected type ${ty}`);
88+
}
89+
// In JavaScript, await automatically also awaits any results of
90+
// awaits, so if an async function returns a future, it will await
91+
// the inner future too. This is not what we want so we
92+
// temporarily put it into a list to protect it.
93+
const wrapped = await_fut(fut);
94+
// complete case, get result / error and print it.
95+
try {
96+
const [value] = await wrapped;
97+
if (value !== undefined) {
98+
echo(
99+
repr_shorten.callKwargs(value, {
100+
separator: "\n<long output truncated>\n",
101+
}),
102+
);
103+
}
104+
if (value instanceof pyodide.ffi.PyProxy) {
105+
value.destroy();
106+
}
107+
} catch (e) {
108+
if (e.constructor.name === "PythonError") {
109+
const message = fut.formatted_error || e.message;
110+
term.error(message.trimEnd());
111+
} else {
112+
throw e;
113+
}
114+
} finally {
115+
fut.destroy();
116+
wrapped.destroy();
117+
}
118+
}
119+
term.resume();
120+
await sleep(10);
121+
unlock();
122+
}
123+
124+
pyconsole.stdout_callback = (s) => echo(s, { newline: false });
125+
pyconsole.stderr_callback = (s) => {
126+
term.error(s.trimEnd());
127+
};
128+
129+
return interpreter;
130+
}
131+
23132
export const PlaygroundTerminal: React.FC = () => {
24133
const terminalRef = useRef(null);
25134

26-
const {state} = useEnvironmentSetup();
135+
const {state, pyodide} = useEnvironmentSetup();
136+
137+
const setupComplete = useRef<boolean>(false);
27138

28139
useEffect(() => {
29-
terminalRef.current.echo("Setting up environment");
30-
terminalRef.current.freeze();
140+
terminalRef.current.echo("Setting up environment...");
141+
terminalRef.current.freeze(true);
142+
terminalRef.current.setPrompt("");
31143
}, [])
32144

33145
useEffect(() => {
34-
if (state.pyodideStatus !== "done unpacking") {
146+
if (state.environmentStatus === EnvironmentStatus.WaitBitbakeOrPyodide) {
147+
let s = "";
148+
switch (state.pyodideStatus) {
149+
case PyodideStatus.Idle:
150+
s = "idle";
151+
break;
152+
case PyodideStatus.Fetching:
153+
s = "fetching...";
154+
break;
155+
case PyodideStatus.Loading:
156+
s = "loading...";
157+
break;
158+
case PyodideStatus.Done:
159+
s = "done!";
160+
break;
161+
case PyodideStatus.Inactive:
162+
s = "inactive";
163+
break;
164+
}
165+
35166
terminalRef.current.setPrompt(
36-
`Downloading bitbake: ${progress(state.bitbakeProgress, 80)}%\nPyodide: ${state.pyodideStatus}`
167+
`Downloading BitBake: ${progress(state.bitbakeProgress, 30)}%\nPyodide: ${s}`
37168
)
38169
} else {
39-
terminalRef.current.setPrompt(
40-
`Done unpacking BitBake`
41-
)
42-
}
43-
}, [state]);
170+
switch (state.environmentStatus) {
171+
case EnvironmentStatus.UnpackingBitbake:
172+
terminalRef.current.setPrompt(
173+
`Unpacking BitBake...`
174+
)
175+
break;
176+
case EnvironmentStatus.LoadingSqlite3:
177+
terminalRef.current.setPrompt(
178+
`Loading sqlite3...`
179+
)
180+
break;
181+
case EnvironmentStatus.Configuring:
182+
terminalRef.current.setPrompt(
183+
`Installing import hooks...`
184+
)
185+
break;
186+
case EnvironmentStatus.ImportingBitbake:
187+
terminalRef.current.setPrompt(
188+
`Importing BitBake...`
189+
)
190+
break;
191+
case EnvironmentStatus.Ready:
192+
if (!setupComplete.current) {
193+
setupComplete.current = true;
44194

45-
const interpreter = (command, term) => {
195+
terminalRef.current.setInterpreter(create_interpreter(pyodide, terminalRef.current));
46196

47-
};
197+
terminalRef.current.echo("Ready :)\n");
198+
terminalRef.current.setPrompt(">>> ");
199+
terminalRef.current.freeze(false);
200+
}
201+
break;
202+
}
203+
}
204+
}, [pyodide, state]);
48205

49-
return (<JQueryTerminal interpreter={interpreter} ref={terminalRef}/>)
206+
return (<JQueryTerminal ref={terminalRef}/>)
50207
}

src/hooks/useEnvironmentSetup.ts

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,26 @@
11
import {useImmerReducer} from "use-immer";
2-
import {usePyodide} from "./usePyodide";
3-
import {useEffect} from "react";
2+
import {PyodideStatus, usePyodide} from "./usePyodide";
3+
import {useEffect, useRef} from "react";
44
import {useSWRProgress} from "./useSWRProgress";
55

6+
export enum EnvironmentStatus {
7+
WaitBitbakeOrPyodide,
8+
LoadingSqlite3,
9+
UnpackingBitbake,
10+
Configuring,
11+
ImportingBitbake,
12+
Ready,
13+
}
614

15+
enum InternalStatus {
16+
NotRun,
17+
Running,
18+
Done
19+
}
720

821
const initialEnvironmentState = {
9-
pyodideStatus: "idle",
22+
environmentStatus: EnvironmentStatus.WaitBitbakeOrPyodide,
23+
pyodideStatus: PyodideStatus.Idle,
1024
bitbakeProgress: 0
1125
};
1226

@@ -18,6 +32,9 @@ function reducer(draft, action) {
1832
case "bitbakeProgressChanged":
1933
draft.bitbakeProgress = action.bitbakeProgress;
2034
return;
35+
case "environmentStatusChanged":
36+
draft.environmentStatus = action.environmentStatus;
37+
break;
2138
}
2239
}
2340

@@ -33,19 +50,21 @@ export const useEnvironmentSetup = () => {
3350

3451
useEffect(() => {
3552
dispatch({type: "bitbakeProgressChanged", bitbakeProgress: progress});
36-
}, [progress, dispatch]);
53+
}, [dispatch, progress]);
54+
55+
const effectStatus = useRef<InternalStatus>(InternalStatus.NotRun);
3756

3857
useEffect(() => {
39-
if (pyodide && data) {
40-
dispatch({type: "pyodideStatusChanged", pyodideStatus: "Loading sqlite3"});
58+
if (pyodideStatus === PyodideStatus.Done && progress === 100 && effectStatus.current === InternalStatus.NotRun) {
59+
effectStatus.current = InternalStatus.Running;
4160
const f = async() => {
61+
dispatch({type: "environmentStatusChanged", environmentStatus: EnvironmentStatus.LoadingSqlite3});
4262
await pyodide.loadPackage("sqlite3");
43-
dispatch({type: "pyodideStatusChanged", pyodideStatus: "done sqlite3"});
44-
dispatch({type: "pyodideStatusChanged", pyodideStatus: "Unpacking..."});
63+
dispatch({type: "environmentStatusChanged", pyodideStatus: EnvironmentStatus.UnpackingBitbake});
4564
pyodide.unpackArchive(data, "zip", {
4665
extractDir: "bb"
4766
});
48-
dispatch({type: "pyodideStatusChanged", pyodideStatus: "done unpacking"});
67+
dispatch({type: "environmentStatusChanged", pyodideStatus: EnvironmentStatus.Configuring});
4968

5069
pyodide.runPython(`
5170
import os.path
@@ -157,31 +176,34 @@ sys.meta_path.append(BuiltinImporterShim())
157176
158177
print(sys.meta_path)
159178
`)
160-
const file = pyodide.FS.readdir("./bb");
161-
console.log(file);
179+
// const file = pyodide.FS.readdir("./bb");
180+
// console.log(file);
162181

182+
dispatch({type: "environmentStatusChanged", environmentStatus: EnvironmentStatus.ImportingBitbake});
163183
pyodide.runPython(`
164184
import sys
165185
sys.path.insert(0, "./bb/bitbake-2.8.0/lib/")
166186
from bb.data_smart import DataSmart
167187
`)
168188

169-
const DataSmart = pyodide.globals.get('DataSmart');
170-
const d = DataSmart();
171-
172-
d.setVar("A", "B");
173-
d.setVar("A:test", "C");
174-
d.setVar("OVERRIDES", "test");
175-
d.setVarFlag("A", "p", "OK");
176-
177-
console.log(d.getVar("A"));
178-
179-
DataSmart.destroy();
189+
dispatch({type: "environmentStatusChanged", environmentStatus: EnvironmentStatus.Ready});
190+
// const DataSmart = pyodide.globals.get('DataSmart');
191+
// const d = DataSmart();
192+
//
193+
// d.setVar("A", "B");
194+
// d.setVar("A:test", "C");
195+
// d.setVar("OVERRIDES", "test");
196+
// d.setVarFlag("A", "p", "OK");
197+
//
198+
// console.log(d.getVar("A"));
199+
//
200+
// DataSmart.destroy();
180201
}
181202

182203
f();
204+
effectStatus.current = InternalStatus.Done;
183205
}
184-
}, [data, dispatch, pyodide]);
206+
}, [data, dispatch, progress, pyodide, pyodideStatus]);
185207

186-
return {state};
208+
return {state, pyodide};
187209
};

src/hooks/usePyodide.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,28 @@ import {PyodideInterface} from "pyodide";
44

55
let cachedInstance: PyodideInterface = null;
66

7-
export const usePyodide: () => { pyodide: PyodideInterface; status: string } = () => {
7+
export enum PyodideStatus {
8+
Idle,
9+
Fetching,
10+
Loading,
11+
Done,
12+
Inactive,
13+
}
14+
15+
export const usePyodide: () => { pyodide: PyodideInterface; status: PyodideStatus } = () => {
816
const [pyodide, setPyodide] = useState<PyodideInterface>(null);
9-
const [status, setStatus] = useState<string>('idle');
17+
const [status, setStatus] = useState<PyodideStatus>(PyodideStatus.Idle);
1018

1119
useEffect(() => {
1220
let isActive = true;
1321

1422
const loadPyodide = async () => {
1523
if (!cachedInstance) {
16-
setStatus("importing");
24+
setStatus(PyodideStatus.Fetching);
1725
const { loadPyodide: loadPyodideModule } = await import("https://cdn.jsdelivr.net/pyodide/v0.25.1/full/pyodide.mjs");
18-
setStatus("loading");
26+
setStatus(PyodideStatus.Loading);
1927
cachedInstance = await loadPyodideModule();
20-
setStatus("done");
28+
setStatus(PyodideStatus.Done);
2129
}
2230
if (isActive) {
2331
setPyodide(cachedInstance);
@@ -27,7 +35,7 @@ export const usePyodide: () => { pyodide: PyodideInterface; status: string } = (
2735
loadPyodide();
2836

2937
return () => {
30-
setStatus("inactive");
38+
setStatus(PyodideStatus.Inactive);
3139
isActive = false;
3240
};
3341
}, []);

0 commit comments

Comments
 (0)