bpo-40280: Add limited Emscripten REPL (GH-32284) · python/cpython@96e0983
1+<!DOCTYPE html>
2+<html lang="en">
3+<head>
4+<meta charset="UTF-8">
5+<meta http-equiv="X-UA-Compatible" content="IE=edge">
6+<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+<meta name="author" content="Katie Bell">
8+<meta name="description" content="Simple REPL for Python WASM">
9+<title>wasm-python terminal</title>
10+<link rel="stylesheet" href="https://unpkg.com/xterm@4.18.0/css/xterm.css" crossorigin/>
11+<style>
12+body {
13+font-family: arial;
14+max-width: 800px;
15+margin: 0 auto
16+ }
17+#code {
18+width: 100%;
19+height: 180px;
20+ }
21+#info {
22+padding-top: 20px;
23+ }
24+ .button-container {
25+display: flex;
26+justify-content: end;
27+height: 50px;
28+align-items: center;
29+gap: 10px;
30+ }
31+button {
32+padding: 6px 18px;
33+ }
34+</style>
35+<script src="https://unpkg.com/xterm@4.18.0/lib/xterm.js" crossorigin></script>
36+<script type="module">
37+class WorkerManager {
38+constructor(workerURL, standardIO, readyCallBack) {
39+this.workerURL = workerURL
40+this.worker = null
41+this.standardIO = standardIO
42+this.readyCallBack = readyCallBack
43+44+this.initialiseWorker()
45+}
46+47+async initialiseWorker() {
48+if (!this.worker) {
49+this.worker = new Worker(this.workerURL)
50+this.worker.addEventListener('message', this.handleMessageFromWorker)
51+}
52+}
53+54+async run(options) {
55+this.worker.postMessage({
56+type: 'run',
57+args: options.args || [],
58+files: options.files || {}
59+})
60+}
61+62+handleStdinData(inputValue) {
63+if (this.stdinbuffer && this.stdinbufferInt) {
64+let startingIndex = 1
65+if (this.stdinbufferInt[0] > 0) {
66+startingIndex = this.stdinbufferInt[0]
67+}
68+const data = new TextEncoder().encode(inputValue)
69+data.forEach((value, index) => {
70+this.stdinbufferInt[startingIndex + index] = value
71+})
72+73+this.stdinbufferInt[0] = startingIndex + data.length - 1
74+Atomics.notify(this.stdinbufferInt, 0, 1)
75+}
76+}
77+78+handleMessageFromWorker = (event) => {
79+const type = event.data.type
80+if (type === 'ready') {
81+this.readyCallBack()
82+} else if (type === 'stdout') {
83+this.standardIO.stdout(event.data.stdout)
84+} else if (type === 'stderr') {
85+this.standardIO.stderr(event.data.stderr)
86+} else if (type === 'stdin') {
87+// Leave it to the terminal to decide whether to chunk it into lines
88+// or send characters depending on the use case.
89+this.stdinbuffer = event.data.buffer
90+this.stdinbufferInt = new Int32Array(this.stdinbuffer)
91+this.standardIO.stdin().then((inputValue) => {
92+this.handleStdinData(inputValue)
93+})
94+} else if (type === 'finished') {
95+this.standardIO.stderr(`Exited with status: ${event.data.returnCode}\r\n`)
96+}
97+}
98+}
99+100+class WasmTerminal {
101+102+constructor() {
103+this.input = ''
104+this.resolveInput = null
105+this.activeInput = false
106+this.inputStartCursor = null
107+108+this.xterm = new Terminal(
109+{ scrollback: 10000, fontSize: 14, theme: { background: '#1a1c1f' }, cols: 100}
110+);
111+112+this.xterm.onKey((keyEvent) => {
113+// Fix for iOS Keyboard Jumping on space
114+if (keyEvent.key === " ") {
115+keyEvent.domEvent.preventDefault();
116+}
117+});
118+119+this.xterm.onData(this.handleTermData)
120+}
121+122+open(container) {
123+this.xterm.open(container);
124+}
125+126+handleReadComplete(lastChar) {
127+this.resolveInput(this.input + lastChar)
128+this.activeInput = false
129+}
130+131+handleTermData = (data) => {
132+if (!this.activeInput) {
133+return
134+}
135+const ord = data.charCodeAt(0);
136+let ofs;
137+138+// TODO: Handle ANSI escape sequences
139+if (ord === 0x1b) {
140+// Handle special characters
141+} else if (ord < 32 || ord === 0x7f) {
142+switch (data) {
143+case "\r": // ENTER
144+case "\x0a": // CTRL+J
145+case "\x0d": // CTRL+M
146+this.xterm.write('\r\n');
147+this.handleReadComplete('\n');
148+break;
149+case "\x7F": // BACKSPACE
150+case "\x08": // CTRL+H
151+case "\x04": // CTRL+D
152+this.handleCursorErase(true);
153+break;
154+}
155+} else {
156+this.handleCursorInsert(data);
157+}
158+}
159+160+handleCursorInsert(data) {
161+this.input += data;
162+this.xterm.write(data)
163+}
164+165+handleCursorErase() {
166+// Don't delete past the start of input
167+if (this.xterm.buffer.active.cursorX <= this.inputStartCursor) {
168+return
169+}
170+this.input = this.input.slice(0, -1)
171+this.xterm.write('\x1B[D')
172+this.xterm.write('\x1B[P')
173+}
174+175+prompt = async () => {
176+this.activeInput = true
177+// Hack to allow stdout/stderr to finish before we figure out where input starts
178+setTimeout(() => {this.inputStartCursor = this.xterm.buffer.active.cursorX}, 1)
179+return new Promise((resolve, reject) => {
180+this.resolveInput = (value) => {
181+this.input = ''
182+resolve(value)
183+}
184+})
185+}
186+187+clear() {
188+this.xterm.clear();
189+}
190+191+print(message) {
192+const normInput = message.replace(/[\r\n]+/g, "\n").replace(/\n/g, "\r\n");
193+this.xterm.write(normInput);
194+}
195+}
196+197+const replButton = document.getElementById('repl')
198+const clearButton = document.getElementById('clear')
199+200+window.onload = () => {
201+const terminal = new WasmTerminal()
202+terminal.open(document.getElementById('terminal'))
203+204+const stdio = {
205+stdout: (s) => { terminal.print(s) },
206+stderr: (s) => { terminal.print(s) },
207+stdin: async () => {
208+return await terminal.prompt()
209+}
210+}
211+212+replButton.addEventListener('click', (e) => {
213+// Need to use "-i -" to force interactive mode.
214+// Looks like isatty always returns false in emscripten
215+pythonWorkerManager.run({args: ['-i', '-'], files: {}})
216+})
217+218+clearButton.addEventListener('click', (e) => {
219+terminal.clear()
220+})
221+222+const readyCallback = () => {
223+replButton.removeAttribute('disabled')
224+clearButton.removeAttribute('disabled')
225+}
226+227+const pythonWorkerManager = new WorkerManager('./python.worker.js', stdio, readyCallback)
228+}
229+</script>
230+</head>
231+<body>
232+<h1>Simple REPL for Python WASM</h1>
233+<div id="terminal"></div>
234+<div class="button-container">
235+<button id="repl" disabled>Start REPL</button>
236+<button id="clear" disabled>Clear</button>
237+</div>
238+<div id="info">
239+ The simple REPL provides a limited Python experience in the browser.
240+<a href="https://github.com/python/cpython/blob/main/Tools/wasm/README.md">
241+ Tools/wasm/README.md</a> contains a list of known limitations and
242+ issues. Networking, subprocesses, and threading are not available.
243+</div>
244+</body>
245+</html>