mirror of https://github.com/python/cpython
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.
This commit is contained in:
parent
0d07182821
commit
010aaa32fb
|
@ -35,11 +35,12 @@
|
|||
<script src="https://unpkg.com/xterm@4.18.0/lib/xterm.js" crossorigin integrity="sha384-yYdNmem1ioP5Onm7RpXutin5A8TimLheLNQ6tnMi01/ZpxXdAwIm2t4fJMx1Djs+"/></script>
|
||||
<script type="module">
|
||||
class WorkerManager {
|
||||
constructor(workerURL, standardIO, readyCallBack) {
|
||||
constructor(workerURL, standardIO, readyCallBack, finishedCallback) {
|
||||
this.workerURL = workerURL
|
||||
this.worker = null
|
||||
this.standardIO = standardIO
|
||||
this.readyCallBack = readyCallBack
|
||||
this.finishedCallback = finishedCallback
|
||||
|
||||
this.initialiseWorker()
|
||||
}
|
||||
|
@ -59,6 +60,15 @@ class WorkerManager {
|
|||
})
|
||||
}
|
||||
|
||||
reset() {
|
||||
if (this.worker) {
|
||||
this.worker.terminate()
|
||||
this.worker = null
|
||||
}
|
||||
this.standardIO.message('Worker process terminated.')
|
||||
this.initialiseWorker()
|
||||
}
|
||||
|
||||
handleStdinData(inputValue) {
|
||||
if (this.stdinbuffer && this.stdinbufferInt) {
|
||||
let startingIndex = 1
|
||||
|
@ -92,7 +102,8 @@ class WorkerManager {
|
|||
this.handleStdinData(inputValue)
|
||||
})
|
||||
} else if (type === 'finished') {
|
||||
this.standardIO.stderr(`Exited with status: ${event.data.returnCode}\r\n`)
|
||||
this.standardIO.message(`Exited with status: ${event.data.returnCode}`)
|
||||
this.finishedCallback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -168,9 +179,14 @@ class WasmTerminal {
|
|||
break;
|
||||
case "\x7F": // BACKSPACE
|
||||
case "\x08": // CTRL+H
|
||||
case "\x04": // CTRL+D
|
||||
this.handleCursorErase(true);
|
||||
break;
|
||||
case "\x04": // CTRL+D
|
||||
// Send empty input
|
||||
if (this.input === '') {
|
||||
this.resolveInput('')
|
||||
this.activeInput = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.handleCursorInsert(data);
|
||||
|
@ -265,9 +281,13 @@ class BufferQueue {
|
|||
}
|
||||
}
|
||||
|
||||
const runButton = document.getElementById('run')
|
||||
const replButton = document.getElementById('repl')
|
||||
const stopButton = document.getElementById('stop')
|
||||
const clearButton = document.getElementById('clear')
|
||||
|
||||
const codeBox = document.getElementById('codebox')
|
||||
|
||||
window.onload = () => {
|
||||
const terminal = new WasmTerminal()
|
||||
terminal.open(document.getElementById('terminal'))
|
||||
|
@ -277,35 +297,72 @@ window.onload = () => {
|
|||
stderr: (charCode) => { terminal.print(charCode) },
|
||||
stdin: async () => {
|
||||
return await terminal.prompt()
|
||||
},
|
||||
message: (text) => { terminal.writeLine(`\r\n${text}\r\n`) },
|
||||
}
|
||||
|
||||
const programRunning = (isRunning) => {
|
||||
if (isRunning) {
|
||||
replButton.setAttribute('disabled', true)
|
||||
runButton.setAttribute('disabled', true)
|
||||
stopButton.removeAttribute('disabled')
|
||||
} else {
|
||||
replButton.removeAttribute('disabled')
|
||||
runButton.removeAttribute('disabled')
|
||||
stopButton.setAttribute('disabled', true)
|
||||
}
|
||||
}
|
||||
|
||||
runButton.addEventListener('click', (e) => {
|
||||
terminal.clear()
|
||||
programRunning(true)
|
||||
const code = codeBox.value
|
||||
pythonWorkerManager.run({args: ['main.py'], files: {'main.py': code}})
|
||||
})
|
||||
|
||||
replButton.addEventListener('click', (e) => {
|
||||
terminal.clear()
|
||||
programRunning(true)
|
||||
// Need to use "-i -" to force interactive mode.
|
||||
// Looks like isatty always returns false in emscripten
|
||||
pythonWorkerManager.run({args: ['-i', '-'], files: {}})
|
||||
})
|
||||
|
||||
stopButton.addEventListener('click', (e) => {
|
||||
programRunning(false)
|
||||
pythonWorkerManager.reset()
|
||||
})
|
||||
|
||||
clearButton.addEventListener('click', (e) => {
|
||||
terminal.clear()
|
||||
})
|
||||
|
||||
const readyCallback = () => {
|
||||
replButton.removeAttribute('disabled')
|
||||
runButton.removeAttribute('disabled')
|
||||
clearButton.removeAttribute('disabled')
|
||||
}
|
||||
|
||||
const pythonWorkerManager = new WorkerManager('./python.worker.js', stdio, readyCallback)
|
||||
const finishedCallback = () => {
|
||||
programRunning(false)
|
||||
}
|
||||
|
||||
const pythonWorkerManager = new WorkerManager('./python.worker.js', stdio, readyCallback, finishedCallback)
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Simple REPL for Python WASM</h1>
|
||||
<div id="terminal"></div>
|
||||
<textarea id="codebox" cols="108" rows="16">
|
||||
print('Welcome to WASM!')
|
||||
</textarea>
|
||||
<div class="button-container">
|
||||
<button id="run" disabled>Run</button>
|
||||
<button id="repl" disabled>Start REPL</button>
|
||||
<button id="stop" disabled>Stop</button>
|
||||
<button id="clear" disabled>Clear</button>
|
||||
</div>
|
||||
<div id="terminal"></div>
|
||||
<div id="info">
|
||||
The simple REPL provides a limited Python experience in the browser.
|
||||
<a href="https://github.com/python/cpython/blob/main/Tools/wasm/README.md">
|
||||
|
|
|
@ -19,18 +19,18 @@ class StdinBuffer {
|
|||
}
|
||||
|
||||
stdin = () => {
|
||||
if (this.numberOfCharacters + 1 === this.readIndex) {
|
||||
while (this.numberOfCharacters + 1 === this.readIndex) {
|
||||
if (!this.sentNull) {
|
||||
// Must return null once to indicate we're done for now.
|
||||
this.sentNull = true
|
||||
return null
|
||||
}
|
||||
this.sentNull = false
|
||||
// Prompt will reset this.readIndex to 1
|
||||
this.prompt()
|
||||
}
|
||||
const char = this.buffer[this.readIndex]
|
||||
this.readIndex += 1
|
||||
// How do I send an EOF??
|
||||
return char
|
||||
}
|
||||
}
|
||||
|
@ -71,7 +71,11 @@ var Module = {
|
|||
|
||||
onmessage = (event) => {
|
||||
if (event.data.type === 'run') {
|
||||
// TODO: Set up files from event.data.files
|
||||
if (event.data.files) {
|
||||
for (const [filename, contents] of Object.entries(event.data.files)) {
|
||||
Module.FS.writeFile(filename, contents)
|
||||
}
|
||||
}
|
||||
const ret = callMain(event.data.args)
|
||||
postMessage({
|
||||
type: 'finished',
|
||||
|
|
Loading…
Reference in New Issue