17db96d56Sopenharmony_ci<!DOCTYPE html> 27db96d56Sopenharmony_ci<html lang="en"> 37db96d56Sopenharmony_ci<head> 47db96d56Sopenharmony_ci <meta charset="UTF-8"> 57db96d56Sopenharmony_ci <meta http-equiv="X-UA-Compatible" content="IE=edge"> 67db96d56Sopenharmony_ci <meta name="viewport" content="width=device-width, initial-scale=1.0"> 77db96d56Sopenharmony_ci <meta name="author" content="Katie Bell"> 87db96d56Sopenharmony_ci <meta name="description" content="Simple REPL for Python WASM"> 97db96d56Sopenharmony_ci <title>wasm-python terminal</title> 107db96d56Sopenharmony_ci <link rel="stylesheet" href="https://unpkg.com/xterm@4.18.0/css/xterm.css" crossorigin integrity="sha384-4eEEn/eZgVHkElpKAzzPx/Kow/dTSgFk1BNe+uHdjHa+NkZJDh5Vqkq31+y7Eycd"/> 117db96d56Sopenharmony_ci <style> 127db96d56Sopenharmony_ci body { 137db96d56Sopenharmony_ci font-family: arial; 147db96d56Sopenharmony_ci max-width: 800px; 157db96d56Sopenharmony_ci margin: 0 auto 167db96d56Sopenharmony_ci } 177db96d56Sopenharmony_ci #code { 187db96d56Sopenharmony_ci width: 100%; 197db96d56Sopenharmony_ci height: 180px; 207db96d56Sopenharmony_ci } 217db96d56Sopenharmony_ci #info { 227db96d56Sopenharmony_ci padding-top: 20px; 237db96d56Sopenharmony_ci } 247db96d56Sopenharmony_ci .button-container { 257db96d56Sopenharmony_ci display: flex; 267db96d56Sopenharmony_ci justify-content: end; 277db96d56Sopenharmony_ci height: 50px; 287db96d56Sopenharmony_ci align-items: center; 297db96d56Sopenharmony_ci gap: 10px; 307db96d56Sopenharmony_ci } 317db96d56Sopenharmony_ci button { 327db96d56Sopenharmony_ci padding: 6px 18px; 337db96d56Sopenharmony_ci } 347db96d56Sopenharmony_ci </style> 357db96d56Sopenharmony_ci <script src="https://unpkg.com/xterm@4.18.0/lib/xterm.js" crossorigin integrity="sha384-yYdNmem1ioP5Onm7RpXutin5A8TimLheLNQ6tnMi01/ZpxXdAwIm2t4fJMx1Djs+"/></script> 367db96d56Sopenharmony_ci <script type="module"> 377db96d56Sopenharmony_ciclass WorkerManager { 387db96d56Sopenharmony_ci constructor(workerURL, standardIO, readyCallBack) { 397db96d56Sopenharmony_ci this.workerURL = workerURL 407db96d56Sopenharmony_ci this.worker = null 417db96d56Sopenharmony_ci this.standardIO = standardIO 427db96d56Sopenharmony_ci this.readyCallBack = readyCallBack 437db96d56Sopenharmony_ci 447db96d56Sopenharmony_ci this.initialiseWorker() 457db96d56Sopenharmony_ci } 467db96d56Sopenharmony_ci 477db96d56Sopenharmony_ci async initialiseWorker() { 487db96d56Sopenharmony_ci if (!this.worker) { 497db96d56Sopenharmony_ci this.worker = new Worker(this.workerURL) 507db96d56Sopenharmony_ci this.worker.addEventListener('message', this.handleMessageFromWorker) 517db96d56Sopenharmony_ci } 527db96d56Sopenharmony_ci } 537db96d56Sopenharmony_ci 547db96d56Sopenharmony_ci async run(options) { 557db96d56Sopenharmony_ci this.worker.postMessage({ 567db96d56Sopenharmony_ci type: 'run', 577db96d56Sopenharmony_ci args: options.args || [], 587db96d56Sopenharmony_ci files: options.files || {} 597db96d56Sopenharmony_ci }) 607db96d56Sopenharmony_ci } 617db96d56Sopenharmony_ci 627db96d56Sopenharmony_ci handleStdinData(inputValue) { 637db96d56Sopenharmony_ci if (this.stdinbuffer && this.stdinbufferInt) { 647db96d56Sopenharmony_ci let startingIndex = 1 657db96d56Sopenharmony_ci if (this.stdinbufferInt[0] > 0) { 667db96d56Sopenharmony_ci startingIndex = this.stdinbufferInt[0] 677db96d56Sopenharmony_ci } 687db96d56Sopenharmony_ci const data = new TextEncoder().encode(inputValue) 697db96d56Sopenharmony_ci data.forEach((value, index) => { 707db96d56Sopenharmony_ci this.stdinbufferInt[startingIndex + index] = value 717db96d56Sopenharmony_ci }) 727db96d56Sopenharmony_ci 737db96d56Sopenharmony_ci this.stdinbufferInt[0] = startingIndex + data.length - 1 747db96d56Sopenharmony_ci Atomics.notify(this.stdinbufferInt, 0, 1) 757db96d56Sopenharmony_ci } 767db96d56Sopenharmony_ci } 777db96d56Sopenharmony_ci 787db96d56Sopenharmony_ci handleMessageFromWorker = (event) => { 797db96d56Sopenharmony_ci const type = event.data.type 807db96d56Sopenharmony_ci if (type === 'ready') { 817db96d56Sopenharmony_ci this.readyCallBack() 827db96d56Sopenharmony_ci } else if (type === 'stdout') { 837db96d56Sopenharmony_ci this.standardIO.stdout(event.data.stdout) 847db96d56Sopenharmony_ci } else if (type === 'stderr') { 857db96d56Sopenharmony_ci this.standardIO.stderr(event.data.stderr) 867db96d56Sopenharmony_ci } else if (type === 'stdin') { 877db96d56Sopenharmony_ci // Leave it to the terminal to decide whether to chunk it into lines 887db96d56Sopenharmony_ci // or send characters depending on the use case. 897db96d56Sopenharmony_ci this.stdinbuffer = event.data.buffer 907db96d56Sopenharmony_ci this.stdinbufferInt = new Int32Array(this.stdinbuffer) 917db96d56Sopenharmony_ci this.standardIO.stdin().then((inputValue) => { 927db96d56Sopenharmony_ci this.handleStdinData(inputValue) 937db96d56Sopenharmony_ci }) 947db96d56Sopenharmony_ci } else if (type === 'finished') { 957db96d56Sopenharmony_ci this.standardIO.stderr(`Exited with status: ${event.data.returnCode}\r\n`) 967db96d56Sopenharmony_ci } 977db96d56Sopenharmony_ci } 987db96d56Sopenharmony_ci} 997db96d56Sopenharmony_ci 1007db96d56Sopenharmony_ciclass WasmTerminal { 1017db96d56Sopenharmony_ci 1027db96d56Sopenharmony_ci constructor() { 1037db96d56Sopenharmony_ci this.inputBuffer = new BufferQueue(); 1047db96d56Sopenharmony_ci this.input = '' 1057db96d56Sopenharmony_ci this.resolveInput = null 1067db96d56Sopenharmony_ci this.activeInput = false 1077db96d56Sopenharmony_ci this.inputStartCursor = null 1087db96d56Sopenharmony_ci 1097db96d56Sopenharmony_ci this.xterm = new Terminal( 1107db96d56Sopenharmony_ci { scrollback: 10000, fontSize: 14, theme: { background: '#1a1c1f' }, cols: 100} 1117db96d56Sopenharmony_ci ); 1127db96d56Sopenharmony_ci 1137db96d56Sopenharmony_ci this.xterm.onKey((keyEvent) => { 1147db96d56Sopenharmony_ci // Fix for iOS Keyboard Jumping on space 1157db96d56Sopenharmony_ci if (keyEvent.key === " ") { 1167db96d56Sopenharmony_ci keyEvent.domEvent.preventDefault(); 1177db96d56Sopenharmony_ci } 1187db96d56Sopenharmony_ci }); 1197db96d56Sopenharmony_ci 1207db96d56Sopenharmony_ci this.xterm.onData(this.handleTermData) 1217db96d56Sopenharmony_ci } 1227db96d56Sopenharmony_ci 1237db96d56Sopenharmony_ci open(container) { 1247db96d56Sopenharmony_ci this.xterm.open(container); 1257db96d56Sopenharmony_ci } 1267db96d56Sopenharmony_ci 1277db96d56Sopenharmony_ci handleTermData = (data) => { 1287db96d56Sopenharmony_ci const ord = data.charCodeAt(0); 1297db96d56Sopenharmony_ci data = data.replace(/\r(?!\n)/g, "\n") // Convert lone CRs to LF 1307db96d56Sopenharmony_ci 1317db96d56Sopenharmony_ci // Handle pasted data 1327db96d56Sopenharmony_ci if (data.length > 1 && data.includes("\n")) { 1337db96d56Sopenharmony_ci let alreadyWrittenChars = 0; 1347db96d56Sopenharmony_ci // If line already had data on it, merge pasted data with it 1357db96d56Sopenharmony_ci if (this.input != '') { 1367db96d56Sopenharmony_ci this.inputBuffer.addData(this.input); 1377db96d56Sopenharmony_ci alreadyWrittenChars = this.input.length; 1387db96d56Sopenharmony_ci this.input = ''; 1397db96d56Sopenharmony_ci } 1407db96d56Sopenharmony_ci this.inputBuffer.addData(data); 1417db96d56Sopenharmony_ci // If input is active, write the first line 1427db96d56Sopenharmony_ci if (this.activeInput) { 1437db96d56Sopenharmony_ci let line = this.inputBuffer.nextLine(); 1447db96d56Sopenharmony_ci this.writeLine(line.slice(alreadyWrittenChars)); 1457db96d56Sopenharmony_ci this.resolveInput(line); 1467db96d56Sopenharmony_ci this.activeInput = false; 1477db96d56Sopenharmony_ci } 1487db96d56Sopenharmony_ci // When input isn't active, add to line buffer 1497db96d56Sopenharmony_ci } else if (!this.activeInput) { 1507db96d56Sopenharmony_ci // Skip non-printable characters 1517db96d56Sopenharmony_ci if (!(ord === 0x1b || ord == 0x7f || ord < 32)) { 1527db96d56Sopenharmony_ci this.inputBuffer.addData(data); 1537db96d56Sopenharmony_ci } 1547db96d56Sopenharmony_ci // TODO: Handle ANSI escape sequences 1557db96d56Sopenharmony_ci } else if (ord === 0x1b) { 1567db96d56Sopenharmony_ci // Handle special characters 1577db96d56Sopenharmony_ci } else if (ord < 32 || ord === 0x7f) { 1587db96d56Sopenharmony_ci switch (data) { 1597db96d56Sopenharmony_ci case "\x0c": // CTRL+L 1607db96d56Sopenharmony_ci this.clear(); 1617db96d56Sopenharmony_ci break; 1627db96d56Sopenharmony_ci case "\n": // ENTER 1637db96d56Sopenharmony_ci case "\x0a": // CTRL+J 1647db96d56Sopenharmony_ci case "\x0d": // CTRL+M 1657db96d56Sopenharmony_ci this.resolveInput(this.input + this.writeLine('\n')); 1667db96d56Sopenharmony_ci this.input = ''; 1677db96d56Sopenharmony_ci this.activeInput = false; 1687db96d56Sopenharmony_ci break; 1697db96d56Sopenharmony_ci case "\x7F": // BACKSPACE 1707db96d56Sopenharmony_ci case "\x08": // CTRL+H 1717db96d56Sopenharmony_ci case "\x04": // CTRL+D 1727db96d56Sopenharmony_ci this.handleCursorErase(true); 1737db96d56Sopenharmony_ci break; 1747db96d56Sopenharmony_ci } 1757db96d56Sopenharmony_ci } else { 1767db96d56Sopenharmony_ci this.handleCursorInsert(data); 1777db96d56Sopenharmony_ci } 1787db96d56Sopenharmony_ci } 1797db96d56Sopenharmony_ci 1807db96d56Sopenharmony_ci writeLine(line) { 1817db96d56Sopenharmony_ci this.xterm.write(line.slice(0, -1)) 1827db96d56Sopenharmony_ci this.xterm.write('\r\n'); 1837db96d56Sopenharmony_ci return line; 1847db96d56Sopenharmony_ci } 1857db96d56Sopenharmony_ci 1867db96d56Sopenharmony_ci handleCursorInsert(data) { 1877db96d56Sopenharmony_ci this.input += data; 1887db96d56Sopenharmony_ci this.xterm.write(data) 1897db96d56Sopenharmony_ci } 1907db96d56Sopenharmony_ci 1917db96d56Sopenharmony_ci handleCursorErase() { 1927db96d56Sopenharmony_ci // Don't delete past the start of input 1937db96d56Sopenharmony_ci if (this.xterm.buffer.active.cursorX <= this.inputStartCursor) { 1947db96d56Sopenharmony_ci return 1957db96d56Sopenharmony_ci } 1967db96d56Sopenharmony_ci this.input = this.input.slice(0, -1) 1977db96d56Sopenharmony_ci this.xterm.write('\x1B[D') 1987db96d56Sopenharmony_ci this.xterm.write('\x1B[P') 1997db96d56Sopenharmony_ci } 2007db96d56Sopenharmony_ci 2017db96d56Sopenharmony_ci prompt = async () => { 2027db96d56Sopenharmony_ci this.activeInput = true 2037db96d56Sopenharmony_ci // Hack to allow stdout/stderr to finish before we figure out where input starts 2047db96d56Sopenharmony_ci setTimeout(() => {this.inputStartCursor = this.xterm.buffer.active.cursorX}, 1) 2057db96d56Sopenharmony_ci // If line buffer has a line ready, send it immediately 2067db96d56Sopenharmony_ci if (this.inputBuffer.hasLineReady()) { 2077db96d56Sopenharmony_ci return new Promise((resolve, reject) => { 2087db96d56Sopenharmony_ci resolve(this.writeLine(this.inputBuffer.nextLine())); 2097db96d56Sopenharmony_ci this.activeInput = false; 2107db96d56Sopenharmony_ci }) 2117db96d56Sopenharmony_ci // If line buffer has an incomplete line, use it for the active line 2127db96d56Sopenharmony_ci } else if (this.inputBuffer.lastLineIsIncomplete()) { 2137db96d56Sopenharmony_ci // Hack to ensure cursor input start doesn't end up after user input 2147db96d56Sopenharmony_ci setTimeout(() => {this.handleCursorInsert(this.inputBuffer.nextLine())}, 1); 2157db96d56Sopenharmony_ci } 2167db96d56Sopenharmony_ci return new Promise((resolve, reject) => { 2177db96d56Sopenharmony_ci this.resolveInput = (value) => { 2187db96d56Sopenharmony_ci resolve(value) 2197db96d56Sopenharmony_ci } 2207db96d56Sopenharmony_ci }) 2217db96d56Sopenharmony_ci } 2227db96d56Sopenharmony_ci 2237db96d56Sopenharmony_ci clear() { 2247db96d56Sopenharmony_ci this.xterm.clear(); 2257db96d56Sopenharmony_ci } 2267db96d56Sopenharmony_ci 2277db96d56Sopenharmony_ci print(charCode) { 2287db96d56Sopenharmony_ci let array = [charCode]; 2297db96d56Sopenharmony_ci if (charCode == 10) { 2307db96d56Sopenharmony_ci array = [13, 10]; // Replace \n with \r\n 2317db96d56Sopenharmony_ci } 2327db96d56Sopenharmony_ci this.xterm.write(new Uint8Array(array)); 2337db96d56Sopenharmony_ci } 2347db96d56Sopenharmony_ci} 2357db96d56Sopenharmony_ci 2367db96d56Sopenharmony_ciclass BufferQueue { 2377db96d56Sopenharmony_ci constructor(xterm) { 2387db96d56Sopenharmony_ci this.buffer = [] 2397db96d56Sopenharmony_ci } 2407db96d56Sopenharmony_ci 2417db96d56Sopenharmony_ci isEmpty() { 2427db96d56Sopenharmony_ci return this.buffer.length == 0 2437db96d56Sopenharmony_ci } 2447db96d56Sopenharmony_ci 2457db96d56Sopenharmony_ci lastLineIsIncomplete() { 2467db96d56Sopenharmony_ci return !this.isEmpty() && !this.buffer[this.buffer.length-1].endsWith("\n") 2477db96d56Sopenharmony_ci } 2487db96d56Sopenharmony_ci 2497db96d56Sopenharmony_ci hasLineReady() { 2507db96d56Sopenharmony_ci return !this.isEmpty() && this.buffer[0].endsWith("\n") 2517db96d56Sopenharmony_ci } 2527db96d56Sopenharmony_ci 2537db96d56Sopenharmony_ci addData(data) { 2547db96d56Sopenharmony_ci let lines = data.match(/.*(\n|$)/g) 2557db96d56Sopenharmony_ci if (this.lastLineIsIncomplete()) { 2567db96d56Sopenharmony_ci this.buffer[this.buffer.length-1] += lines.shift() 2577db96d56Sopenharmony_ci } 2587db96d56Sopenharmony_ci for (let line of lines) { 2597db96d56Sopenharmony_ci this.buffer.push(line) 2607db96d56Sopenharmony_ci } 2617db96d56Sopenharmony_ci } 2627db96d56Sopenharmony_ci 2637db96d56Sopenharmony_ci nextLine() { 2647db96d56Sopenharmony_ci return this.buffer.shift() 2657db96d56Sopenharmony_ci } 2667db96d56Sopenharmony_ci} 2677db96d56Sopenharmony_ci 2687db96d56Sopenharmony_ciconst replButton = document.getElementById('repl') 2697db96d56Sopenharmony_ciconst clearButton = document.getElementById('clear') 2707db96d56Sopenharmony_ci 2717db96d56Sopenharmony_ciwindow.onload = () => { 2727db96d56Sopenharmony_ci const terminal = new WasmTerminal() 2737db96d56Sopenharmony_ci terminal.open(document.getElementById('terminal')) 2747db96d56Sopenharmony_ci 2757db96d56Sopenharmony_ci const stdio = { 2767db96d56Sopenharmony_ci stdout: (charCode) => { terminal.print(charCode) }, 2777db96d56Sopenharmony_ci stderr: (charCode) => { terminal.print(charCode) }, 2787db96d56Sopenharmony_ci stdin: async () => { 2797db96d56Sopenharmony_ci return await terminal.prompt() 2807db96d56Sopenharmony_ci } 2817db96d56Sopenharmony_ci } 2827db96d56Sopenharmony_ci 2837db96d56Sopenharmony_ci replButton.addEventListener('click', (e) => { 2847db96d56Sopenharmony_ci // Need to use "-i -" to force interactive mode. 2857db96d56Sopenharmony_ci // Looks like isatty always returns false in emscripten 2867db96d56Sopenharmony_ci pythonWorkerManager.run({args: ['-i', '-'], files: {}}) 2877db96d56Sopenharmony_ci }) 2887db96d56Sopenharmony_ci 2897db96d56Sopenharmony_ci clearButton.addEventListener('click', (e) => { 2907db96d56Sopenharmony_ci terminal.clear() 2917db96d56Sopenharmony_ci }) 2927db96d56Sopenharmony_ci 2937db96d56Sopenharmony_ci const readyCallback = () => { 2947db96d56Sopenharmony_ci replButton.removeAttribute('disabled') 2957db96d56Sopenharmony_ci clearButton.removeAttribute('disabled') 2967db96d56Sopenharmony_ci } 2977db96d56Sopenharmony_ci 2987db96d56Sopenharmony_ci const pythonWorkerManager = new WorkerManager('./python.worker.js', stdio, readyCallback) 2997db96d56Sopenharmony_ci} 3007db96d56Sopenharmony_ci </script> 3017db96d56Sopenharmony_ci</head> 3027db96d56Sopenharmony_ci<body> 3037db96d56Sopenharmony_ci <h1>Simple REPL for Python WASM</h1> 3047db96d56Sopenharmony_ci <div id="terminal"></div> 3057db96d56Sopenharmony_ci <div class="button-container"> 3067db96d56Sopenharmony_ci <button id="repl" disabled>Start REPL</button> 3077db96d56Sopenharmony_ci <button id="clear" disabled>Clear</button> 3087db96d56Sopenharmony_ci </div> 3097db96d56Sopenharmony_ci <div id="info"> 3107db96d56Sopenharmony_ci The simple REPL provides a limited Python experience in the browser. 3117db96d56Sopenharmony_ci <a href="https://github.com/python/cpython/blob/main/Tools/wasm/README.md"> 3127db96d56Sopenharmony_ci Tools/wasm/README.md</a> contains a list of known limitations and 3137db96d56Sopenharmony_ci issues. Networking, subprocesses, and threading are not available. 3147db96d56Sopenharmony_ci </div> 3157db96d56Sopenharmony_ci</body> 3167db96d56Sopenharmony_ci</html> 317