]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-97747: Improvements to WASM browser REPL. (#97665)
authorKatie Bell <katie@katharos.id.au>
Fri, 31 May 2024 07:58:46 +0000 (17:58 +1000)
committerGitHub <noreply@github.com>
Fri, 31 May 2024 07:58:46 +0000 (09:58 +0200)
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.

Tools/wasm/python.html
Tools/wasm/python.worker.js

index 17ffa0ea8bfeff0fa4cd945328e709dc1a2dc34d..81a035a5c4cd93be90a6fa1ef028f9ce19052c94 100644 (file)
     <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">
index 1b794608fffe7befdd8cdfd3e2e3bad3114f964a..4ce4e16fc0fa19e943161557820f35ad5a99149e 100644 (file)
@@ -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',