mirror of
				https://github.com/python/cpython.git
				synced 2025-11-04 11:49:12 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			245 lines
		
	
	
	
		
			7.3 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			245 lines
		
	
	
	
		
			7.3 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
<!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>
 |