mirror of https://github.com/python/cpython
246 lines
7.3 KiB
HTML
246 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>
|