Skip to content

Commit 70c8fea

Browse files
committed
WIP: Testing REPL Capabilities
1 parent beb617c commit 70c8fea

File tree

6 files changed

+375
-0
lines changed

6 files changed

+375
-0
lines changed
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Polyscript - MicroPython REPL</title>
7+
<style>
8+
*, *::before, *::after {
9+
box-sizing: border-box;
10+
}
11+
html {
12+
background-color: black;
13+
color: white;
14+
}
15+
</style>
16+
<script type="module">
17+
import { define } from '../../../index.js';
18+
import createTerminal from './repl.js';
19+
20+
define(null, {
21+
interpreter: 'micropython',
22+
hooks: {
23+
main: {
24+
onReady: async wrap => {
25+
const terminal = await createTerminal(wrap, output);
26+
globalThis.terminal = terminal;
27+
}
28+
}
29+
}
30+
});
31+
</script>
32+
</head>
33+
<body>
34+
<pre id="output"></pre>
35+
</body>
36+
</html>
+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Xterm.js dependencies via CDN
2+
const CDN = 'https://cdn.jsdelivr.net/npm';
3+
const XTERM = '5.3.0';
4+
const ADDON_FIT = '0.10.0';
5+
const ADDON_WEB_LINKS = '0.11.0';
6+
7+
const { assign } = Object;
8+
9+
const dependencies = ({ ownerDocument }) => {
10+
const rel = 'stylesheet';
11+
const href = `${CDN}/xterm@${XTERM}/css/xterm.min.css`;
12+
const link = `link[rel="${rel}"][href="${href}"]`;
13+
if (!ownerDocument.querySelector(link)) {
14+
ownerDocument.head.append(
15+
assign(ownerDocument.createElement('link'), { rel, href })
16+
);
17+
}
18+
return [
19+
import(`${CDN}/xterm@${XTERM}/+esm`),
20+
import(`${CDN}/@xterm/addon-fit@${ADDON_FIT}/+esm`),
21+
import(`${CDN}/@xterm/addon-web-links@${ADDON_WEB_LINKS}/+esm`),
22+
];
23+
};
24+
25+
export default async ({ interpreter, io }, target) => {
26+
const [
27+
{ Terminal },
28+
{ FitAddon },
29+
{ WebLinksAddon },
30+
] = await Promise.all(dependencies(target));
31+
32+
const terminal = new Terminal({
33+
cursorBlink: true,
34+
cursorStyle: "block",
35+
theme: {
36+
background: "#191A19",
37+
foreground: "#F5F2E7",
38+
},
39+
});
40+
41+
const encoder = new TextEncoderStream;
42+
encoder.readable.pipeTo(
43+
new WritableStream({
44+
write(buffer) {
45+
for (const c of buffer)
46+
interpreter.replProcessChar(c);
47+
}
48+
})
49+
);
50+
51+
const missingReturn = new Uint8Array([13]);
52+
io.stdout = buffer => {
53+
// apparently Python swallows \r on output
54+
// so we need to monkey patch this when \n
55+
// is sent but no \r was previously added
56+
if (buffer[0] === 10)
57+
terminal.write(missingReturn);
58+
terminal.write(buffer);
59+
};
60+
61+
const writer = encoder.writable.getWriter();
62+
terminal.onData(buffer => writer.write(buffer));
63+
64+
const fitAddon = new FitAddon;
65+
terminal.loadAddon(fitAddon);
66+
terminal.loadAddon(new WebLinksAddon);
67+
terminal.open(target);
68+
fitAddon.fit();
69+
terminal.focus();
70+
71+
interpreter.replInit();
72+
73+
return terminal;
74+
};

test/repl/micropython/index.html

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Polyscript - MicroPython REPL</title>
7+
<style>
8+
*, *::before, *::after {
9+
box-sizing: border-box;
10+
}
11+
html {
12+
background-color: black;
13+
color: white;
14+
}
15+
</style>
16+
<script type="module">
17+
import { define } from '../../../dist/index.js';
18+
import createTerminal from './repl.js';
19+
20+
define(null, {
21+
interpreter: 'micropython',
22+
hooks: {
23+
main: {
24+
onReady: async wrap => {
25+
const terminal = await createTerminal(wrap, output);
26+
globalThis.terminal = terminal;
27+
}
28+
}
29+
}
30+
});
31+
</script>
32+
</head>
33+
<body>
34+
<pre id="output"></pre>
35+
</body>
36+
</html>

test/repl/micropython/repl.js

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Xterm.js dependencies via CDN
2+
const CDN = 'https://cdn.jsdelivr.net/npm';
3+
const XTERM = '5.3.0';
4+
const ADDON_FIT = '0.10.0';
5+
const ADDON_WEB_LINKS = '0.11.0';
6+
7+
const { assign } = Object;
8+
9+
const dependencies = ({ ownerDocument }) => {
10+
const rel = 'stylesheet';
11+
const href = `${CDN}/xterm@${XTERM}/css/xterm.min.css`;
12+
const link = `link[rel="${rel}"][href="${href}"]`;
13+
if (!ownerDocument.querySelector(link)) {
14+
ownerDocument.head.append(
15+
assign(ownerDocument.createElement('link'), { rel, href })
16+
);
17+
}
18+
return [
19+
import(`${CDN}/xterm@${XTERM}/+esm`),
20+
import(`${CDN}/@xterm/addon-fit@${ADDON_FIT}/+esm`),
21+
import(`${CDN}/@xterm/addon-web-links@${ADDON_WEB_LINKS}/+esm`),
22+
];
23+
};
24+
25+
export default async ({ interpreter, io }, target) => {
26+
const [
27+
{ Terminal },
28+
{ FitAddon },
29+
{ WebLinksAddon },
30+
] = await Promise.all(dependencies(target));
31+
32+
const terminal = new Terminal({
33+
cursorBlink: true,
34+
cursorStyle: "block",
35+
theme: {
36+
background: "#191A19",
37+
foreground: "#F5F2E7",
38+
},
39+
});
40+
41+
const encoder = new TextEncoderStream;
42+
encoder.readable.pipeTo(
43+
new WritableStream({
44+
write(buffer) {
45+
for (const c of buffer)
46+
interpreter.replProcessChar(c);
47+
}
48+
})
49+
);
50+
51+
const missingReturn = new Uint8Array([13]);
52+
io.stdout = buffer => {
53+
// apparently Python swallows \r on output
54+
// so we need to monkey patch this when \n
55+
// is sent but no \r was previously added
56+
if (buffer[0] === 10)
57+
terminal.write(missingReturn);
58+
terminal.write(buffer);
59+
};
60+
61+
const writer = encoder.writable.getWriter();
62+
terminal.onData(buffer => writer.write(buffer));
63+
64+
const fitAddon = new FitAddon;
65+
terminal.loadAddon(fitAddon);
66+
terminal.loadAddon(new WebLinksAddon);
67+
terminal.open(target);
68+
fitAddon.fit();
69+
terminal.focus();
70+
71+
interpreter.replInit();
72+
73+
return terminal;
74+
};

test/repl/pyodide/index.html

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Polyscript - Pyodide REPL</title>
7+
<style>
8+
*, *::before, *::after {
9+
box-sizing: border-box;
10+
}
11+
html {
12+
background-color: black;
13+
color: white;
14+
}
15+
</style>
16+
<script type="module">
17+
import { define } from '../../../dist/index.js';
18+
import createTerminal from './repl.js';
19+
20+
define(null, {
21+
interpreter: 'pyodide',
22+
hooks: {
23+
main: {
24+
onReady: async wrap => {
25+
const terminal = await createTerminal(wrap, output);
26+
globalThis.terminal = terminal;
27+
}
28+
}
29+
}
30+
});
31+
</script>
32+
</head>
33+
<body>
34+
<pre id="output"></pre>
35+
</body>
36+
</html>

test/repl/pyodide/repl.js

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Xterm.js dependencies via CDN
2+
const CDN = 'https://cdn.jsdelivr.net/npm';
3+
const XTERM = '5.3.0';
4+
const ADDON_FIT = '0.10.0';
5+
const ADDON_WEB_LINKS = '0.11.0';
6+
const READLINE = '1.1.1';
7+
8+
const { assign } = Object;
9+
10+
const dependencies = ({ ownerDocument }) => {
11+
const rel = 'stylesheet';
12+
const href = `${CDN}/xterm@${XTERM}/css/xterm.min.css`;
13+
const link = `link[rel="${rel}"][href="${href}"]`;
14+
if (!ownerDocument.querySelector(link)) {
15+
ownerDocument.head.append(
16+
assign(ownerDocument.createElement('link'), { rel, href })
17+
);
18+
}
19+
return [
20+
import(`${CDN}/xterm@${XTERM}/+esm`),
21+
import(`${CDN}/@xterm/addon-fit@${ADDON_FIT}/+esm`),
22+
import(`${CDN}/@xterm/addon-web-links@${ADDON_WEB_LINKS}/+esm`),
23+
import(`${CDN}/xterm-readline@${READLINE}/+esm`),
24+
];
25+
};
26+
27+
export default async ({ interpreter, run }, target) => {
28+
const libs = dependencies(target);
29+
30+
const namespace = interpreter.globals.get('dict')();
31+
32+
run(`
33+
import sys
34+
from pyodide.ffi import to_js
35+
from pyodide.console import PyodideConsole, repr_shorten, BANNER
36+
import __main__
37+
BANNER = "Welcome to the Pyodide terminal emulator 🐍\\n" + BANNER
38+
pyconsole = PyodideConsole(__main__.__dict__)
39+
import builtins
40+
async def await_fut(fut):
41+
res = await fut
42+
if res is not None:
43+
builtins._ = res
44+
return to_js([res], depth=1)
45+
def clear_console():
46+
pyconsole.buffer = []
47+
`, { globals: namespace });
48+
49+
const repr_shorten = namespace.get('repr_shorten');
50+
const banner = namespace.get('BANNER');
51+
const await_fut = namespace.get('await_fut');
52+
const pyconsole = namespace.get('pyconsole');
53+
54+
const [
55+
{ Terminal },
56+
{ FitAddon },
57+
{ WebLinksAddon },
58+
] = await Promise.all(libs);
59+
60+
const terminal = new Terminal({
61+
cursorBlink: true,
62+
cursorStyle: "block",
63+
theme: {
64+
background: "#191A19",
65+
foreground: "#F5F2E7",
66+
},
67+
});
68+
69+
let queue = Promise.resolve('');
70+
let acc = '';
71+
terminal.onData(buffer => {
72+
terminal.write(buffer);
73+
acc += buffer;
74+
if (acc.endsWith('\r')) {
75+
const line = acc;
76+
acc = '';
77+
const fut = pyconsole.push(line);
78+
const wrapped = await_fut(fut);
79+
queue = queue.then(async () => {
80+
try {
81+
const [ value ] = await wrapped;
82+
terminal.write('\r\n');
83+
if (value) {
84+
repr_shorten.callKwargs(value, {
85+
separator: "\n<long output truncated>\n",
86+
});
87+
terminal.write(String(value) + '\r\n>>> ');
88+
}
89+
else {
90+
terminal.write('... ');
91+
}
92+
if (value instanceof interpreter.ffi.PyProxy)
93+
value.destroy();
94+
}
95+
catch(e) {
96+
if (e.constructor.name === "PythonError") {
97+
const message = fut.formatted_error || e.message;
98+
terminal.write(message.trimEnd().replace(/\n/g, '\r\n>>> '));
99+
} else {
100+
throw e;
101+
}
102+
}
103+
finally {
104+
fut.destroy();
105+
wrapped.destroy();
106+
}
107+
});
108+
}
109+
});
110+
111+
const fitAddon = new FitAddon;
112+
terminal.loadAddon(fitAddon);
113+
terminal.loadAddon(new WebLinksAddon);
114+
terminal.open(target);
115+
fitAddon.fit();
116+
terminal.focus();
117+
118+
return terminal;
119+
};

0 commit comments

Comments
 (0)