diff options
Diffstat (limited to 'Tools/wasm/python.html')
-rw-r--r-- | Tools/wasm/python.html | 245 |
1 files changed, 245 insertions, 0 deletions
diff --git a/Tools/wasm/python.html b/Tools/wasm/python.html new file mode 100644 index 0000000..c8d1748 --- /dev/null +++ b/Tools/wasm/python.html @@ -0,0 +1,245 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="author" content="Katie Bell"> + <meta name="description" content="Simple REPL for Python WASM"> + <title>wasm-python terminal</title> + <link rel="stylesheet" href="https://unpkg.com/xterm@4.18.0/css/xterm.css" crossorigin/> + <style> + body { + font-family: arial; + max-width: 800px; + margin: 0 auto + } + #code { + width: 100%; + height: 180px; + } + #info { + padding-top: 20px; + } + .button-container { + display: flex; + justify-content: end; + height: 50px; + align-items: center; + gap: 10px; + } + button { + padding: 6px 18px; + } + </style> + <script src="https://unpkg.com/xterm@4.18.0/lib/xterm.js" crossorigin></script> + <script type="module"> +class WorkerManager { + constructor(workerURL, standardIO, readyCallBack) { + this.workerURL = workerURL + this.worker = null + this.standardIO = standardIO + this.readyCallBack = readyCallBack + + this.initialiseWorker() + } + + async initialiseWorker() { + if (!this.worker) { + this.worker = new Worker(this.workerURL) + this.worker.addEventListener('message', this.handleMessageFromWorker) + } + } + + async run(options) { + this.worker.postMessage({ + type: 'run', + args: options.args || [], + files: options.files || {} + }) + } + + handleStdinData(inputValue) { + if (this.stdinbuffer && this.stdinbufferInt) { + let startingIndex = 1 + if (this.stdinbufferInt[0] > 0) { + startingIndex = this.stdinbufferInt[0] + } + const data = new TextEncoder().encode(inputValue) + data.forEach((value, index) => { + this.stdinbufferInt[startingIndex + index] = value + }) + + this.stdinbufferInt[0] = startingIndex + data.length - 1 + Atomics.notify(this.stdinbufferInt, 0, 1) + } + } + + handleMessageFromWorker = (event) => { + const type = event.data.type + if (type === 'ready') { + this.readyCallBack() + } else if (type === 'stdout') { + this.standardIO.stdout(event.data.stdout) + } else if (type === 'stderr') { + this.standardIO.stderr(event.data.stderr) + } else if (type === 'stdin') { + // Leave it to the terminal to decide whether to chunk it into lines + // or send characters depending on the use case. + this.stdinbuffer = event.data.buffer + this.stdinbufferInt = new Int32Array(this.stdinbuffer) + this.standardIO.stdin().then((inputValue) => { + this.handleStdinData(inputValue) + }) + } else if (type === 'finished') { + this.standardIO.stderr(`Exited with status: ${event.data.returnCode}\r\n`) + } + } +} + +class WasmTerminal { + + constructor() { + this.input = '' + this.resolveInput = null + this.activeInput = false + this.inputStartCursor = null + + this.xterm = new Terminal( + { scrollback: 10000, fontSize: 14, theme: { background: '#1a1c1f' }, cols: 100} + ); + + this.xterm.onKey((keyEvent) => { + // Fix for iOS Keyboard Jumping on space + if (keyEvent.key === " ") { + keyEvent.domEvent.preventDefault(); + } + }); + + this.xterm.onData(this.handleTermData) + } + + open(container) { + this.xterm.open(container); + } + + handleReadComplete(lastChar) { + this.resolveInput(this.input + lastChar) + this.activeInput = false + } + + handleTermData = (data) => { + if (!this.activeInput) { + return + } + const ord = data.charCodeAt(0); + let ofs; + + // TODO: Handle ANSI escape sequences + if (ord === 0x1b) { + // Handle special characters + } else if (ord < 32 || ord === 0x7f) { + switch (data) { + case "\r": // ENTER + case "\x0a": // CTRL+J + case "\x0d": // CTRL+M + this.xterm.write('\r\n'); + this.handleReadComplete('\n'); + break; + case "\x7F": // BACKSPACE + case "\x08": // CTRL+H + case "\x04": // CTRL+D + this.handleCursorErase(true); + break; + } + } else { + this.handleCursorInsert(data); + } + } + + handleCursorInsert(data) { + this.input += data; + this.xterm.write(data) + } + + handleCursorErase() { + // Don't delete past the start of input + if (this.xterm.buffer.active.cursorX <= this.inputStartCursor) { + return + } + this.input = this.input.slice(0, -1) + this.xterm.write('\x1B[D') + this.xterm.write('\x1B[P') + } + + prompt = async () => { + this.activeInput = true + // Hack to allow stdout/stderr to finish before we figure out where input starts + setTimeout(() => {this.inputStartCursor = this.xterm.buffer.active.cursorX}, 1) + return new Promise((resolve, reject) => { + this.resolveInput = (value) => { + this.input = '' + resolve(value) + } + }) + } + + clear() { + this.xterm.clear(); + } + + print(message) { + const normInput = message.replace(/[\r\n]+/g, "\n").replace(/\n/g, "\r\n"); + this.xterm.write(normInput); + } +} + +const replButton = document.getElementById('repl') +const clearButton = document.getElementById('clear') + +window.onload = () => { + const terminal = new WasmTerminal() + terminal.open(document.getElementById('terminal')) + + const stdio = { + stdout: (s) => { terminal.print(s) }, + stderr: (s) => { terminal.print(s) }, + stdin: async () => { + return await terminal.prompt() + } + } + + replButton.addEventListener('click', (e) => { + // Need to use "-i -" to force interactive mode. + // Looks like isatty always returns false in emscripten + pythonWorkerManager.run({args: ['-i', '-'], files: {}}) + }) + + clearButton.addEventListener('click', (e) => { + terminal.clear() + }) + + const readyCallback = () => { + replButton.removeAttribute('disabled') + clearButton.removeAttribute('disabled') + } + + const pythonWorkerManager = new WorkerManager('./python.worker.js', stdio, readyCallback) +} + </script> +</head> +<body> + <h1>Simple REPL for Python WASM</h1> + <div id="terminal"></div> + <div class="button-container"> + <button id="repl" disabled>Start REPL</button> + <button id="clear" disabled>Clear</button> + </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"> + Tools/wasm/README.md</a> contains a list of known limitations and + issues. Networking, subprocesses, and threading are not available. + </div> +</body> +</html> |