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