Skip to content

Commit 010aaa3

Browse files
authored
gh-97747: Improvements to WASM browser REPL. (#97665)
Improvements to WASM browser REPL. Adds a text box to write and run code outside the REPL, a stop button, and handling of Ctrl-D for EOF.
1 parent 0d07182 commit 010aaa3

File tree

2 files changed

+69
-8
lines changed

2 files changed

+69
-8
lines changed

Tools/wasm/python.html

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,12 @@
3535
<script src="https://unpkg.com/[email protected]/lib/xterm.js" crossorigin integrity="sha384-yYdNmem1ioP5Onm7RpXutin5A8TimLheLNQ6tnMi01/ZpxXdAwIm2t4fJMx1Djs+"/></script>
3636
<script type="module">
3737
class WorkerManager {
38-
constructor(workerURL, standardIO, readyCallBack) {
38+
constructor(workerURL, standardIO, readyCallBack, finishedCallback) {
3939
this.workerURL = workerURL
4040
this.worker = null
4141
this.standardIO = standardIO
4242
this.readyCallBack = readyCallBack
43+
this.finishedCallback = finishedCallback
4344

4445
this.initialiseWorker()
4546
}
@@ -59,6 +60,15 @@
5960
})
6061
}
6162

63+
reset() {
64+
if (this.worker) {
65+
this.worker.terminate()
66+
this.worker = null
67+
}
68+
this.standardIO.message('Worker process terminated.')
69+
this.initialiseWorker()
70+
}
71+
6272
handleStdinData(inputValue) {
6373
if (this.stdinbuffer && this.stdinbufferInt) {
6474
let startingIndex = 1
@@ -92,7 +102,8 @@
92102
this.handleStdinData(inputValue)
93103
})
94104
} else if (type === 'finished') {
95-
this.standardIO.stderr(`Exited with status: ${event.data.returnCode}\r\n`)
105+
this.standardIO.message(`Exited with status: ${event.data.returnCode}`)
106+
this.finishedCallback()
96107
}
97108
}
98109
}
@@ -168,9 +179,14 @@
168179
break;
169180
case "\x7F": // BACKSPACE
170181
case "\x08": // CTRL+H
171-
case "\x04": // CTRL+D
172182
this.handleCursorErase(true);
173183
break;
184+
case "\x04": // CTRL+D
185+
// Send empty input
186+
if (this.input === '') {
187+
this.resolveInput('')
188+
this.activeInput = false;
189+
}
174190
}
175191
} else {
176192
this.handleCursorInsert(data);
@@ -265,9 +281,13 @@
265281
}
266282
}
267283

284+
const runButton = document.getElementById('run')
268285
const replButton = document.getElementById('repl')
286+
const stopButton = document.getElementById('stop')
269287
const clearButton = document.getElementById('clear')
270288

289+
const codeBox = document.getElementById('codebox')
290+
271291
window.onload = () => {
272292
const terminal = new WasmTerminal()
273293
terminal.open(document.getElementById('terminal'))
@@ -277,35 +297,72 @@
277297
stderr: (charCode) => { terminal.print(charCode) },
278298
stdin: async () => {
279299
return await terminal.prompt()
300+
},
301+
message: (text) => { terminal.writeLine(`\r\n${text}\r\n`) },
302+
}
303+
304+
const programRunning = (isRunning) => {
305+
if (isRunning) {
306+
replButton.setAttribute('disabled', true)
307+
runButton.setAttribute('disabled', true)
308+
stopButton.removeAttribute('disabled')
309+
} else {
310+
replButton.removeAttribute('disabled')
311+
runButton.removeAttribute('disabled')
312+
stopButton.setAttribute('disabled', true)
280313
}
281314
}
282315

316+
runButton.addEventListener('click', (e) => {
317+
terminal.clear()
318+
programRunning(true)
319+
const code = codeBox.value
320+
pythonWorkerManager.run({args: ['main.py'], files: {'main.py': code}})
321+
})
322+
283323
replButton.addEventListener('click', (e) => {
324+
terminal.clear()
325+
programRunning(true)
284326
// Need to use "-i -" to force interactive mode.
285327
// Looks like isatty always returns false in emscripten
286328
pythonWorkerManager.run({args: ['-i', '-'], files: {}})
287329
})
288330

331+
stopButton.addEventListener('click', (e) => {
332+
programRunning(false)
333+
pythonWorkerManager.reset()
334+
})
335+
289336
clearButton.addEventListener('click', (e) => {
290337
terminal.clear()
291338
})
292339

293340
const readyCallback = () => {
294341
replButton.removeAttribute('disabled')
342+
runButton.removeAttribute('disabled')
295343
clearButton.removeAttribute('disabled')
296344
}
297345

298-
const pythonWorkerManager = new WorkerManager('./python.worker.js', stdio, readyCallback)
346+
const finishedCallback = () => {
347+
programRunning(false)
348+
}
349+
350+
const pythonWorkerManager = new WorkerManager('./python.worker.js', stdio, readyCallback, finishedCallback)
299351
}
300352
</script>
301353
</head>
302354
<body>
303355
<h1>Simple REPL for Python WASM</h1>
304-
<div id="terminal"></div>
356+
<textarea id="codebox" cols="108" rows="16">
357+
print('Welcome to WASM!')
358+
</textarea>
305359
<div class="button-container">
360+
<button id="run" disabled>Run</button>
306361
<button id="repl" disabled>Start REPL</button>
362+
<button id="stop" disabled>Stop</button>
307363
<button id="clear" disabled>Clear</button>
308364
</div>
365+
<div id="terminal"></div>
309366
<div id="info">
310367
The simple REPL provides a limited Python experience in the browser.
311368
<a href="https://github.com/python/cpython/blob/main/Tools/wasm/README.md">

Tools/wasm/python.worker.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,18 @@ class StdinBuffer {
1919
}
2020

2121
stdin = () => {
22-
if (this.numberOfCharacters + 1 === this.readIndex) {
22+
while (this.numberOfCharacters + 1 === this.readIndex) {
2323
if (!this.sentNull) {
2424
// Must return null once to indicate we're done for now.
2525
this.sentNull = true
2626
return null
2727
}
2828
this.sentNull = false
29+
// Prompt will reset this.readIndex to 1
2930
this.prompt()
3031
}
3132
const char = this.buffer[this.readIndex]
3233
this.readIndex += 1
33-
// How do I send an EOF??
3434
return char
3535
}
3636
}
@@ -71,7 +71,11 @@ var Module = {
7171

7272
onmessage = (event) => {
7373
if (event.data.type === 'run') {
74-
// TODO: Set up files from event.data.files
74+
if (event.data.files) {
75+
for (const [filename, contents] of Object.entries(event.data.files)) {
76+
Module.FS.writeFile(filename, contents)
77+
}
78+
}
7579
const ret = callMain(event.data.args)
7680
postMessage({
7781
type: 'finished',

0 commit comments

Comments
 (0)