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